fix(installation-demo): synchronize clockwise screw and tool playback
This commit is contained in:
parent
89eb66c6f6
commit
0509df6408
@ -438,6 +438,7 @@ export function InstallationPreviewStage({
|
||||
removedToolNodeNames: toolAttachmentRemovedNodeNames,
|
||||
followHardwarePath: view === 'preview' && Boolean(playbackAction),
|
||||
screwDriveProgress: view === 'preview' && playbackAction ? playbackScrewDriveProgress : undefined,
|
||||
playbackStep: view === 'preview' ? activeStep : null,
|
||||
visible: toolAttachmentsVisible,
|
||||
sceneKey: `${modelUrl ?? 'no-model'}:${modelNodeNames.join('\u001f')}`,
|
||||
onAssetLoadError: handleToolAssetLoadError,
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
} from '@/lib/installation-demo-tool-motion'
|
||||
import type {
|
||||
InstallationPartItem,
|
||||
InstallationStepDraft,
|
||||
InstallationToolActionTransform,
|
||||
InstallationToolItem,
|
||||
} from '../types'
|
||||
@ -26,6 +27,11 @@ import {
|
||||
resolveInstallationToolFollowTransform,
|
||||
} from '../lib/installationNodeGeometry'
|
||||
import { isInstallationToolObjAsset } from '../lib/installationToolAssetFormat'
|
||||
import { getPartNodeNames } from '../lib/installationPartCatalogHelpers'
|
||||
import {
|
||||
buildInstallationScrewDriveAxes,
|
||||
resolveInstallationScrewDriveWorldAxis,
|
||||
} from '../lib/installationScrewDriveAxes'
|
||||
import {
|
||||
prepareInstallationToolModel,
|
||||
resolveInstallationToolBoundsPoint,
|
||||
@ -63,6 +69,7 @@ interface InstallationToolAttachmentOptions {
|
||||
removedToolNodeNames: string[]
|
||||
followHardwarePath: boolean
|
||||
screwDriveProgress?: number
|
||||
playbackStep?: InstallationStepDraft | null
|
||||
visible: boolean
|
||||
sceneKey: string
|
||||
onAssetLoadError?: (tool: InstallationToolItem) => void
|
||||
@ -147,6 +154,7 @@ export function useInstallationToolAttachments({
|
||||
removedToolNodeNames,
|
||||
followHardwarePath,
|
||||
screwDriveProgress,
|
||||
playbackStep = null,
|
||||
visible,
|
||||
sceneKey,
|
||||
onAssetLoadError,
|
||||
@ -157,6 +165,11 @@ export function useInstallationToolAttachments({
|
||||
const toolMaterialConfigsRef = useRef(toolMaterialConfigsByToolId)
|
||||
const followHardwarePathRef = useRef(followHardwarePath)
|
||||
const screwDriveProgressRef = useRef(screwDriveProgress)
|
||||
const screwDriveAxes = useMemo(() => buildInstallationScrewDriveAxes(
|
||||
playbackStep,
|
||||
hardwareParts.filter(part => part.screwDrive).flatMap(getPartNodeNames),
|
||||
), [hardwareParts, playbackStep])
|
||||
const screwDriveAxesRef = useRef(screwDriveAxes)
|
||||
const assetLoadErrorToolIdsRef = useRef(new Set<string>())
|
||||
const fallbackToolId = tools.length === 1 ? tools[0]?.id : undefined
|
||||
const attachmentIdentities = useMemo(
|
||||
@ -212,6 +225,10 @@ export function useInstallationToolAttachments({
|
||||
screwDriveProgressRef.current = screwDriveProgress
|
||||
}, [screwDriveProgress])
|
||||
|
||||
useEffect(() => {
|
||||
screwDriveAxesRef.current = screwDriveAxes
|
||||
}, [screwDriveAxes])
|
||||
|
||||
useEffect(() => {
|
||||
toolMaterialConfigsRef.current = toolMaterialConfigsByToolId
|
||||
if (!engine) return
|
||||
@ -290,6 +307,11 @@ export function useInstallationToolAttachments({
|
||||
toolSpinTurns: record.toolSpinTurns,
|
||||
toolStrikeDistance: record.toolStrikeDistance,
|
||||
toolMotionProgress: screwDriveProgressRef.current,
|
||||
screwDriveAxisWorld: Object.hasOwn(screwDriveAxesRef.current, record.hardwareNodeName)
|
||||
? resolveInstallationScrewDriveWorldAxis(
|
||||
screwDriveAxesRef.current[record.hardwareNodeName], record.hardwareObject,
|
||||
)
|
||||
: undefined,
|
||||
})
|
||||
record.root.position.copy(transform.position)
|
||||
record.root.quaternion.copy(transform.rotation)
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import * as THREE from 'three'
|
||||
import { isHighlightHelperObjectName } from '@/lib/3d/engine-highlight-resources'
|
||||
import type { InstallationToolMotionType } from '@/lib/installation-demo-tool-motion'
|
||||
import {
|
||||
createInstallationClockwiseSpin,
|
||||
easeInstallationScrewDriveProgress,
|
||||
INSTALLATION_SCREW_DRIVE_TURNS,
|
||||
} from './installationScrewDriveMotion'
|
||||
|
||||
export function resolveInstallationNodeLocalGeometryCenter(object: THREE.Object3D) {
|
||||
object.updateMatrixWorld(true)
|
||||
@ -49,6 +54,7 @@ export function resolveInstallationToolFollowTransform({
|
||||
toolSpinTurns,
|
||||
toolStrikeDistance,
|
||||
toolMotionProgress,
|
||||
screwDriveAxisWorld,
|
||||
}: {
|
||||
hardwareObject: THREE.Object3D
|
||||
hardwareAnchorLocalPosition: THREE.Vector3
|
||||
@ -61,6 +67,7 @@ export function resolveInstallationToolFollowTransform({
|
||||
toolSpinTurns: number
|
||||
toolStrikeDistance: number
|
||||
toolMotionProgress: number | undefined
|
||||
screwDriveAxisWorld?: THREE.Vector3 | null
|
||||
}) {
|
||||
hardwareObject.updateMatrixWorld(true)
|
||||
const currentHardwareAnchorPosition = hardwareAnchorLocalPosition
|
||||
@ -77,18 +84,18 @@ export function resolveInstallationToolFollowTransform({
|
||||
toolMotionProgress > 0 &&
|
||||
toolMotionProgress < 1
|
||||
) {
|
||||
const easedProgress = Math.min(
|
||||
Math.max(toolMotionProgress + 0.11 * Math.sin(Math.PI * 2 * toolMotionProgress), 0),
|
||||
1,
|
||||
)
|
||||
const easedProgress = easeInstallationScrewDriveProgress(toolMotionProgress)
|
||||
const motionAxis = toolMotionAxisLocal
|
||||
.clone()
|
||||
.applyQuaternion(toolStartWorldRotation)
|
||||
.normalize()
|
||||
if (toolMotionType === 'spin') {
|
||||
const spin = new THREE.Quaternion().setFromAxisAngle(
|
||||
motionAxis,
|
||||
-Math.PI * 2 * toolSpinTurns * easedProgress,
|
||||
// A screw-bound tool shares the screw's axis and turn count, including reversed mounts.
|
||||
// null means the screw has no insertion path; undefined is a standalone tool motion.
|
||||
const spin = createInstallationClockwiseSpin(
|
||||
screwDriveAxisWorld === undefined ? motionAxis : screwDriveAxisWorld ?? new THREE.Vector3(),
|
||||
screwDriveAxisWorld === undefined ? toolSpinTurns : INSTALLATION_SCREW_DRIVE_TURNS,
|
||||
easedProgress,
|
||||
)
|
||||
const basePivotOffset = toolMotionPivotLocal
|
||||
.clone()
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
import * as THREE from 'three'
|
||||
import type { InstallationNodeTransformState, InstallationStepDraft } from '../types'
|
||||
import { buildInstallationScrewDriveAxes, resolveInstallationScrewDriveWorldAxis } from './installationScrewDriveAxes'
|
||||
import { buildInstallationStepActionPlaybackFrame, resolveInstallationStepActionPlaybackTimeline } from './installationStepActionPlayback'
|
||||
|
||||
function transform(position: [number, number, number]): InstallationNodeTransformState {
|
||||
return { position, rotation: [0, 0, 0, 1], offset: [0, 0, 0] }
|
||||
}
|
||||
function step(): InstallationStepDraft {
|
||||
return {
|
||||
id: 'step', order: 1, title: '', note: '', direction: 'front', cameraPreset: 'medium', durationMs: 1800,
|
||||
workflowVersion: 2, transformSpace: 'local', fixedNodeNames: ['base'], installNodeNames: [], hardwareNodeNames: ['screw'],
|
||||
scopeNodeNames: ['base', 'screw'],
|
||||
assembledTransforms: { base: transform([0, 0, 0]), screw: transform([0, 0, 0]) },
|
||||
expandedTransforms: { base: transform([20, 30, 40]), screw: transform([20, 30, 100]) },
|
||||
actions: [{ id: 'drive', order: 1, title: '', nodeNames: ['screw'], startTransforms: { screw: transform([20, 30, 100]) } }],
|
||||
}
|
||||
}
|
||||
|
||||
describe('binding tools to the actual screw installation axis', () => {
|
||||
it('uses the expanded installation target and action start, preserving saved data', () => {
|
||||
const saved = step()
|
||||
const before = JSON.stringify(saved)
|
||||
expect(buildInstallationScrewDriveAxes(saved, ['screw', 'missing'])).toEqual({ screw: [0, 0, -1], missing: null })
|
||||
const timeline = resolveInstallationStepActionPlaybackTimeline(saved, undefined, ['screw'])
|
||||
const phase = timeline.phases[0]
|
||||
const progress = ((phase.screwDriveApproachDurationMs ?? 0) + 15) / timeline.durationMs
|
||||
const frame = buildInstallationStepActionPlaybackFrame(saved, progress, ['screw'])
|
||||
const screw = frame.transforms.find(item => item.nodeName === 'screw')!
|
||||
// From the head at +Z, an initial +X mark moves towards -Y: clockwise.
|
||||
const mark = new THREE.Vector3(1, 0, 0).applyQuaternion(new THREE.Quaternion(...screw.rotation))
|
||||
expect(mark.y).toBeLessThan(0)
|
||||
expect(JSON.stringify(saved)).toBe(before)
|
||||
})
|
||||
|
||||
it('converts the installation axis through rotated/scaled parent transforms', () => {
|
||||
const parent = new THREE.Group()
|
||||
parent.rotation.set(0.2, 0.7, -0.3)
|
||||
parent.scale.set(2, 3, 4)
|
||||
parent.position.set(20, 10, 30)
|
||||
const child = new THREE.Object3D()
|
||||
parent.add(child)
|
||||
parent.updateMatrixWorld(true)
|
||||
const expected = new THREE.Vector3(20, 30, 40).applyMatrix4(parent.matrixWorld)
|
||||
.sub(new THREE.Vector3(20, 30, 100).applyMatrix4(parent.matrixWorld)).normalize()
|
||||
expect(resolveInstallationScrewDriveWorldAxis([0, 0, -1], child)!.distanceTo(expected)).toBeLessThan(1e-8)
|
||||
})
|
||||
|
||||
it('does not invent an insertion axis for stationary or held hardware', () => {
|
||||
const saved = step()
|
||||
saved.actions![0].startTransforms.screw = transform([20, 30, 40])
|
||||
expect(buildInstallationScrewDriveAxes(saved, ['screw']).screw).toBeNull()
|
||||
saved.actions![0].startTransforms.screw = transform([20, 30, 100])
|
||||
saved.holdExpandedNodeNames = ['screw']
|
||||
expect(buildInstallationScrewDriveAxes(saved, ['screw']).screw).toBeNull()
|
||||
expect(buildInstallationScrewDriveAxes(null, ['screw'])).toEqual({})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,48 @@
|
||||
import * as THREE from 'three'
|
||||
import type { InstallationStepDraft, InstallationVector3 } from '../types'
|
||||
import {
|
||||
buildInstallationStepActionEditLocalTransforms,
|
||||
buildInstallationStepActionInProgressLocalTransforms,
|
||||
buildInstallationStepActionPreviewEndLocalTransforms,
|
||||
} from './installationStepActionWorkflow'
|
||||
|
||||
export type InstallationScrewDriveAxes = Record<string, InstallationVector3 | null>
|
||||
|
||||
/** Use the same saved start and installation target as screw playback, before final assembly. */
|
||||
export function buildInstallationScrewDriveAxes(
|
||||
step: InstallationStepDraft | null,
|
||||
screwNodeNames: string[],
|
||||
): InstallationScrewDriveAxes {
|
||||
if (!step) return {}
|
||||
const startMap = new Map(buildInstallationStepActionEditLocalTransforms(step).map(item => [item.nodeName, item]))
|
||||
const endMap = new Map(buildInstallationStepActionPreviewEndLocalTransforms(step).map(item => [item.nodeName, item]))
|
||||
const targetMap = new Map(buildInstallationStepActionInProgressLocalTransforms(step).map(item => [item.nodeName, item]))
|
||||
const heldNodes = new Set([
|
||||
...(step.holdExpandedNodeNames ?? []),
|
||||
...(step.hingeCloseActions ?? []).flatMap(action => action.nodeNames),
|
||||
])
|
||||
return Object.fromEntries(screwNodeNames.map(nodeName => {
|
||||
const start = startMap.get(nodeName) ?? endMap.get(nodeName)
|
||||
const end = endMap.get(nodeName) ?? start
|
||||
const installationTarget = targetMap.get(nodeName) ?? end
|
||||
const target = heldNodes.has(nodeName)
|
||||
? step.expandedTransforms?.[nodeName] ?? installationTarget
|
||||
: installationTarget
|
||||
if (!start?.position || !target?.position) return [nodeName, null]
|
||||
const axis = new THREE.Vector3(...target.position).sub(new THREE.Vector3(...start.position))
|
||||
return [nodeName, axis.lengthSq() > 1e-8 ? axis.normalize().toArray() : null]
|
||||
}))
|
||||
}
|
||||
|
||||
export function resolveInstallationScrewDriveWorldAxis(
|
||||
localAxis: InstallationVector3 | null,
|
||||
hardwareObject: THREE.Object3D,
|
||||
) {
|
||||
if (!localAxis) return null
|
||||
const axis = new THREE.Vector3(...localAxis)
|
||||
if (hardwareObject.parent) {
|
||||
hardwareObject.parent.updateWorldMatrix(true, false)
|
||||
axis.transformDirection(hardwareObject.parent.matrixWorld)
|
||||
}
|
||||
return axis
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
import * as THREE from 'three'
|
||||
import type { NodeLocalTransformState } from '@/lib/3d/engine'
|
||||
import { applyInstallationScrewDriveRotation, createInstallationClockwiseSpin, easeInstallationScrewDriveProgress } from './installationScrewDriveMotion'
|
||||
import { resolveInstallationToolFollowTransform } from './installationNodeGeometry'
|
||||
|
||||
const directions: Array<[string, [number, number, number]]> = [
|
||||
['right', [1, 0, 0]], ['left', [-1, 0, 0]],
|
||||
['up', [0, 1, 0]], ['down', [0, -1, 0]],
|
||||
['front', [0, 0, 1]], ['back', [0, 0, -1]],
|
||||
]
|
||||
const state = (position: [number, number, number]): NodeLocalTransformState => ({
|
||||
nodeName: 'screw', position, rotation: [0, 0, 0, 1],
|
||||
})
|
||||
|
||||
describe('clockwise installation from the screw head towards its tip', () => {
|
||||
it.each(directions)('screw and tool turn clockwise together when inserting %s', (_, vector) => {
|
||||
const axis = new THREE.Vector3(...vector)
|
||||
const radial = new THREE.Vector3(1, 1, 1).cross(axis).normalize()
|
||||
const observerNormal = axis.clone().negate()
|
||||
const start = state([0, 0, 0])
|
||||
const end = state(vector)
|
||||
const toolBase = new THREE.Quaternion().setFromEuler(new THREE.Euler(0.4, 0.3, -0.2))
|
||||
const spin = (progress: number) => {
|
||||
const screw = new THREE.Quaternion(...applyInstallationScrewDriveRotation(start, start, end, progress).rotation)
|
||||
const tool = resolveInstallationToolFollowTransform({
|
||||
hardwareObject: new THREE.Object3D(), hardwareAnchorLocalPosition: new THREE.Vector3(),
|
||||
hardwareStartWorldAnchorPosition: new THREE.Vector3(), toolStartWorldPosition: new THREE.Vector3(),
|
||||
toolStartWorldRotation: toolBase, toolMotionType: 'spin', toolMotionPivotLocal: new THREE.Vector3(),
|
||||
// A reversed/configured shaft and different catalogue turn count cannot reverse a bound screw tool.
|
||||
toolMotionAxisLocal: axis.clone().negate(), toolSpinTurns: 8, toolStrikeDistance: 0,
|
||||
toolMotionProgress: progress, screwDriveAxisWorld: axis,
|
||||
}).rotation.multiply(toolBase.clone().invert())
|
||||
expect(tool.angleTo(screw)).toBeLessThan(1e-7)
|
||||
return screw
|
||||
}
|
||||
let previous = radial.clone()
|
||||
for (let frame = 1; frame <= 80; frame++) {
|
||||
const progress = frame / 81
|
||||
const current = radial.clone().applyQuaternion(spin(progress))
|
||||
// A negative screen-plane signed angle is clockwise for an observer at the head.
|
||||
const signedScreenAngle = Math.atan2(observerNormal.dot(previous.clone().cross(current)), previous.dot(current))
|
||||
expect(signedScreenAngle).toBeLessThan(0)
|
||||
previous = current
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves the saved endpoint, geometry pivot and input objects', () => {
|
||||
const start = state([0, 0, 10])
|
||||
const end = state([0, 0, 0])
|
||||
const current = state([0, 0, 5])
|
||||
const snapshot = JSON.stringify({ start, end, current })
|
||||
const pivot = new THREE.Vector3(2, 3, 4)
|
||||
const result = applyInstallationScrewDriveRotation(current, start, end, 0.13, pivot.toArray())
|
||||
const movedCenter = pivot.clone().applyQuaternion(new THREE.Quaternion(...result.rotation)).add(new THREE.Vector3(...result.position))
|
||||
expect(movedCenter.distanceTo(pivot.clone().add(new THREE.Vector3(...current.position)))).toBeLessThan(1e-8)
|
||||
expect(applyInstallationScrewDriveRotation(end, start, end, 1)).toBe(end)
|
||||
expect(applyInstallationScrewDriveRotation(start, start, end, 0)).toBe(start)
|
||||
expect(JSON.stringify({ start, end, current })).toBe(snapshot)
|
||||
})
|
||||
|
||||
it('uses the same catalogue preview direction for an independent tool', () => {
|
||||
const axis = new THREE.Vector3(0, -1, 0)
|
||||
const progress = 0.07
|
||||
const result = resolveInstallationToolFollowTransform({
|
||||
hardwareObject: new THREE.Object3D(), hardwareAnchorLocalPosition: new THREE.Vector3(),
|
||||
hardwareStartWorldAnchorPosition: new THREE.Vector3(), toolStartWorldPosition: new THREE.Vector3(),
|
||||
toolStartWorldRotation: new THREE.Quaternion(), toolMotionType: 'spin',
|
||||
toolMotionPivotLocal: new THREE.Vector3(), toolMotionAxisLocal: axis,
|
||||
toolSpinTurns: 2, toolStrikeDistance: 0, toolMotionProgress: progress,
|
||||
})
|
||||
expect(result.rotation.angleTo(createInstallationClockwiseSpin(axis, 2, easeInstallationScrewDriveProgress(progress)))).toBeLessThan(1e-7)
|
||||
})
|
||||
|
||||
it('does not spin a screw-bound tool with no insertion path', () => {
|
||||
const result = resolveInstallationToolFollowTransform({
|
||||
hardwareObject: new THREE.Object3D(), hardwareAnchorLocalPosition: new THREE.Vector3(),
|
||||
hardwareStartWorldAnchorPosition: new THREE.Vector3(), toolStartWorldPosition: new THREE.Vector3(),
|
||||
toolStartWorldRotation: new THREE.Quaternion(), toolMotionType: 'spin',
|
||||
toolMotionPivotLocal: new THREE.Vector3(1, 2, 3), toolMotionAxisLocal: new THREE.Vector3(1, 0, 0),
|
||||
toolSpinTurns: 3, toolStrikeDistance: 0, toolMotionProgress: 0.13, screwDriveAxisWorld: null,
|
||||
})
|
||||
expect(result.rotation.angleTo(new THREE.Quaternion())).toBe(0)
|
||||
expect(result.position.toArray()).toEqual([0, 0, 0])
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,54 @@
|
||||
import * as THREE from 'three'
|
||||
import type { NodeLocalTransformState } from '@/lib/3d/engine'
|
||||
|
||||
export const INSTALLATION_SCREW_DRIVE_TURNS = 3
|
||||
|
||||
export function easeInstallationScrewDriveProgress(progress: number) {
|
||||
const clamped = THREE.MathUtils.clamp(progress, 0, 1)
|
||||
return THREE.MathUtils.clamp(clamped + 0.11 * Math.sin(Math.PI * 2 * clamped), 0, 1)
|
||||
}
|
||||
|
||||
/** Positive right-hand rotation looks clockwise from the head along the insertion axis. */
|
||||
export function createInstallationClockwiseSpin(
|
||||
insertionAxis: THREE.Vector3,
|
||||
turns: number,
|
||||
progress: number,
|
||||
) {
|
||||
if (insertionAxis.lengthSq() <= 1e-8) return new THREE.Quaternion()
|
||||
return new THREE.Quaternion().setFromAxisAngle(
|
||||
insertionAxis.clone().normalize(),
|
||||
Math.PI * 2 * turns * progress,
|
||||
)
|
||||
}
|
||||
|
||||
export function applyInstallationScrewDriveRotation(
|
||||
transform: NodeLocalTransformState,
|
||||
start: NodeLocalTransformState,
|
||||
end: NodeLocalTransformState,
|
||||
progress: number | undefined,
|
||||
rotationPivot?: [number, number, number],
|
||||
): NodeLocalTransformState {
|
||||
if (progress === undefined || progress <= 0 || progress >= 1) return transform
|
||||
const insertionAxis = new THREE.Vector3(...end.position).sub(new THREE.Vector3(...start.position))
|
||||
if (insertionAxis.lengthSq() <= 1e-8) return transform
|
||||
|
||||
const spin = createInstallationClockwiseSpin(
|
||||
insertionAxis,
|
||||
INSTALLATION_SCREW_DRIVE_TURNS,
|
||||
easeInstallationScrewDriveProgress(progress),
|
||||
)
|
||||
const baseRotation = new THREE.Quaternion(...transform.rotation).normalize()
|
||||
const rotation = spin.multiply(baseRotation)
|
||||
const position = new THREE.Vector3(...transform.position)
|
||||
if (rotationPivot) {
|
||||
const pivot = new THREE.Vector3(...rotationPivot)
|
||||
const basePivotPosition = pivot.clone().applyQuaternion(baseRotation)
|
||||
const spunPivotPosition = pivot.clone().applyQuaternion(rotation)
|
||||
position.add(basePivotPosition.sub(spunPivotPosition))
|
||||
}
|
||||
return {
|
||||
...transform,
|
||||
position: [position.x, position.y, position.z],
|
||||
rotation: [rotation.x, rotation.y, rotation.z, rotation.w],
|
||||
}
|
||||
}
|
||||
@ -21,12 +21,12 @@ import {
|
||||
import { resolveInstallationHingeCloseTransform } from './installationHingeCloseMotion'
|
||||
import { resolveInstallationStepActionCarryInSummary } from './installationStepActionHistory'
|
||||
import { applyHardwareHingePlayback } from './installationHardwareHingeRuntime'
|
||||
import { applyInstallationScrewDriveRotation } from './installationScrewDriveMotion'
|
||||
|
||||
const ACTION_APPEAR_END = 0.22
|
||||
const ACTION_MOTION_START = 0.38
|
||||
const ACTION_MOTION_END = 0.86
|
||||
const SCREW_DRIVE_CONTACT_PROGRESS = 0.62
|
||||
const SCREW_DRIVE_TURNS = 3
|
||||
export const SCREW_DRIVE_DURATION_MS = 1500
|
||||
const SCREW_DRIVE_CONTACT_ACTION_PROGRESS =
|
||||
ACTION_MOTION_START +
|
||||
@ -71,13 +71,6 @@ function easeProgress(progress: number) {
|
||||
return easeInstallationActionMagneticProgress(clampProgress(progress))
|
||||
}
|
||||
|
||||
function easeScrewDriveProgress(progress: number) {
|
||||
const clampedProgress = clampProgress(progress)
|
||||
return clampProgress(
|
||||
clampedProgress + 0.11 * Math.sin(Math.PI * 2 * clampedProgress),
|
||||
)
|
||||
}
|
||||
|
||||
function collectCarryInOnlyActionIds(
|
||||
step: InstallationStepDraft,
|
||||
steps: InstallationStepDraft[],
|
||||
@ -131,48 +124,6 @@ function interpolateTransform(
|
||||
}
|
||||
}
|
||||
|
||||
function applyScrewDriveRotation(
|
||||
transform: NodeLocalTransformState,
|
||||
start: NodeLocalTransformState,
|
||||
end: NodeLocalTransformState,
|
||||
screwDriveProgress: number | undefined,
|
||||
rotationPivot?: [number, number, number],
|
||||
): NodeLocalTransformState {
|
||||
if (
|
||||
screwDriveProgress === undefined ||
|
||||
screwDriveProgress <= 0 ||
|
||||
screwDriveProgress >= 1 ||
|
||||
!start.position ||
|
||||
!end.position
|
||||
) {
|
||||
return transform
|
||||
}
|
||||
|
||||
const insertionAxis = new THREE.Vector3(...end.position).sub(new THREE.Vector3(...start.position))
|
||||
if (insertionAxis.lengthSq() <= 1e-8) return transform
|
||||
|
||||
const spin = new THREE.Quaternion().setFromAxisAngle(
|
||||
insertionAxis.normalize(),
|
||||
-Math.PI * 2 * SCREW_DRIVE_TURNS * easeScrewDriveProgress(screwDriveProgress),
|
||||
)
|
||||
const baseRotation = new THREE.Quaternion(...transform.rotation).normalize()
|
||||
const rotation = spin.multiply(baseRotation)
|
||||
const position = new THREE.Vector3(...transform.position)
|
||||
|
||||
if (rotationPivot) {
|
||||
const pivot = new THREE.Vector3(...rotationPivot)
|
||||
const basePivotPosition = pivot.clone().applyQuaternion(baseRotation)
|
||||
const spunPivotPosition = pivot.clone().applyQuaternion(rotation)
|
||||
position.add(basePivotPosition.sub(spunPivotPosition))
|
||||
}
|
||||
|
||||
return {
|
||||
...transform,
|
||||
position: [position.x, position.y, position.z],
|
||||
rotation: [rotation.x, rotation.y, rotation.z, rotation.w],
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveInstallationStepActionPlaybackTimeline(
|
||||
step: InstallationStepDraft,
|
||||
durationMs?: number,
|
||||
@ -564,7 +515,7 @@ export function buildInstallationStepActionPlaybackFrame(
|
||||
const transform = interpolateTransform(start, installationTarget, motionProgress)
|
||||
return [
|
||||
screwDriveNodeNameSet.has(nodeName) && !isCarryInOnlyAction
|
||||
? applyScrewDriveRotation(
|
||||
? applyInstallationScrewDriveRotation(
|
||||
transform,
|
||||
start,
|
||||
installationTarget,
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
InstallationToolMotionConfig,
|
||||
} from '@/lib/installation-demo-tool-motion'
|
||||
import type { InstallationToolItem } from '../../types'
|
||||
import { createInstallationClockwiseSpin } from '../../lib/installationScrewDriveMotion'
|
||||
import {
|
||||
buildInstallationHingeNodeTransforms,
|
||||
resolveModelLocalBounds,
|
||||
@ -230,9 +231,10 @@ export function InstallationToolMotionPreview({
|
||||
root.position.copy(basePosition)
|
||||
root.quaternion.copy(baseRotation)
|
||||
if (config.motionType === 'spin') {
|
||||
const spin = new THREE.Quaternion().setFromAxisAngle(
|
||||
const spin = createInstallationClockwiseSpin(
|
||||
axis,
|
||||
Math.PI * 2 * config.spinTurns * phase,
|
||||
config.spinTurns,
|
||||
phase,
|
||||
)
|
||||
root.position.copy(pivot.clone().add(basePosition.clone().sub(pivot).applyQuaternion(spin)))
|
||||
root.quaternion.copy(spin.multiply(baseRotation))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user