显示调整
This commit is contained in:
1 parent
178a7d8412
commit
b5ac9552a1
9 files changed
+1064
-81
No files matched your search
+120
-23
@@ -34,9 +34,6 @@
|
||||
<label class="sp-chk"><input type="checkbox" v-model="showEmitter" />发射点</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="showBones" />骨骼连线</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="showSkinMesh" />蒙皮网格</label>
|
||||
<label class="sp-chk" :class="{ disabled: !hasLoadedSpineAnimation }" :title="hasLoadedSpineAnimation ? '显示或隐藏已加载的 Spine 动画资源' : '加载包含动画的 Spine 资源后可用'">
|
||||
<input type="checkbox" v-model="store.settings.showSpineResources" :disabled="!hasLoadedSpineAnimation" />启用 Spine 资源显示
|
||||
</label>
|
||||
<div class="sp-divider"></div>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.tickEnabled" />启用刻度</label>
|
||||
<template v-if="store.settings.tickEnabled">
|
||||
@@ -57,7 +54,7 @@
|
||||
</div>
|
||||
|
||||
<div class="hud" v-if="info">
|
||||
系统: <b>{{ info.systems }}</b> · 粒子(骨骼): <b>{{ info.particles }}</b> · 根骨骼: <b>root</b>
|
||||
系统: <b class="hud-number hud-system-count">{{ info.systems }}</b> · 粒子(骨骼): <b class="hud-number hud-particle-count">{{ info.particles }}</b> · 根骨骼: <b>root</b>
|
||||
<span class="hud-tip">滚轮缩放 · 左键拖动画布</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,8 +75,6 @@ const showSkinMesh = ref(false)
|
||||
const info = ref<{ systems: number; particles: number } | null>(null)
|
||||
const showGrid = ref(true)
|
||||
const showSettings = ref(false)
|
||||
if (store.settings.showSpineResources == null) store.settings.showSpineResources = true
|
||||
const hasLoadedSpineAnimation = computed(() => store.spines.some((spine) => spine.status === 'ready' && spine.animations.length > 0))
|
||||
// 根变换 gizmo 工具:'translate' | 'rotate' | 'scale' | ''(无)
|
||||
const activeTool = ref<'' | 'translate' | 'rotate' | 'scale'>('')
|
||||
function setTool(t: '' | 'translate' | 'rotate' | 'scale') { activeTool.value = activeTool.value === t ? '' : t }
|
||||
@@ -87,6 +82,7 @@ function setTool(t: '' | 'translate' | 'rotate' | 'scale') { activeTool.value =
|
||||
let app: Application | null = null
|
||||
let world: Container | null = null
|
||||
let originGfx: Graphics | null = null
|
||||
let boneFollowTargetGfx: Graphics | null = null
|
||||
let boneGfx: Graphics | null = null
|
||||
let shapeGfx: Graphics | null = null
|
||||
let forceFieldGfx: Graphics | null = null
|
||||
@@ -132,6 +128,8 @@ function createLayers() {
|
||||
world.addChild(axisGfx)
|
||||
spineRuntime = new SpineRuntimeLayer()
|
||||
world.addChild(spineRuntime.container)
|
||||
boneFollowTargetGfx = new Graphics()
|
||||
world.addChild(boneFollowTargetGfx)
|
||||
originGfx = new Graphics()
|
||||
world.addChild(originGfx)
|
||||
shapeGfx = new Graphics() // 发射器形状范围(绿圆/蓝矩形/锥形)
|
||||
@@ -323,7 +321,8 @@ function worldToPathLocal(path: ScenePath, x: number, y: number) {
|
||||
|
||||
function activePath() {
|
||||
if (store.activeObjectType !== 'path') return null
|
||||
return store.paths.find((item) => item.id === store.activeObjectId) || null
|
||||
const path = store.paths.find((item) => item.id === store.activeObjectId) || null
|
||||
return path?.visible === false ? null : path
|
||||
}
|
||||
|
||||
function pathInsertIndex(path: ScenePath, x: number, y: number) {
|
||||
@@ -468,7 +467,7 @@ function installPointerDrag(move: (event: PointerEvent) => void) {
|
||||
function onColliderPointerDown(e: PointerEvent) {
|
||||
if (store.activeObjectType !== 'collision') return false
|
||||
const collider = store.colliders.find((item) => item.id === store.activeObjectId)
|
||||
if (!collider?.enabled) return false
|
||||
if (!collider?.enabled || collider.visible === false) return false
|
||||
const pointer = pointerToWorldMath(e.clientX, e.clientY)
|
||||
const angle = collider.rotation * Math.PI / 180
|
||||
const cos = Math.cos(angle), sin = Math.sin(angle)
|
||||
@@ -494,6 +493,7 @@ function onColliderPointerDown(e: PointerEvent) {
|
||||
/** 命中当前选中系统的力场圆时开始拖拽;返回 true 表示事件已被力场接管。 */
|
||||
function onForceFieldDown(e: PointerEvent) {
|
||||
const sys = store.systems.find((item) => item.id === store.activeId)
|
||||
if (sys?.visible === false) return false
|
||||
const cfg = sys?.config
|
||||
if (!cfg?.forceField || cfg.forceRadius <= 0) return false
|
||||
const startPointer = pointerToWorldMath(e.clientX, e.clientY)
|
||||
@@ -523,6 +523,7 @@ function onForceFieldDown(e: PointerEvent) {
|
||||
/** 命中吸附圆(半径为 0 时使用屏幕 12px 热区)时拖动全局吸附点。 */
|
||||
function onAttractionDown(e: PointerEvent) {
|
||||
const sys = store.systems.find((item) => item.id === store.activeId)
|
||||
if (sys?.visible === false) return false
|
||||
const cfg = sys?.config
|
||||
if (!cfg?.attraction) return false
|
||||
const startPointer = pointerToWorldMath(e.clientX, e.clientY)
|
||||
@@ -565,7 +566,7 @@ function selectedTransformTarget(): SceneTransformTarget | null {
|
||||
const id = store.activeObjectId
|
||||
if (store.activeObjectType === 'particle') {
|
||||
const system = store.systems.find((item) => item.id === id)
|
||||
if (!system) return null
|
||||
if (!system || system.visible === false) return null
|
||||
const config = system.config
|
||||
return {
|
||||
x: config.centerX, y: config.centerY, rotation: config.rootRotation,
|
||||
@@ -581,7 +582,7 @@ function selectedTransformTarget(): SceneTransformTarget | null {
|
||||
? store.paths
|
||||
: store.spines
|
||||
const object = collection.find((item) => item.id === id) as any
|
||||
if (!object) return null
|
||||
if (!object || object.visible === false) return null
|
||||
return {
|
||||
x: Number(object.x) || 0, y: Number(object.y) || 0, rotation: Number(object.rotation) || 0,
|
||||
scaleX: Math.max(0.05, Number(object.scaleX) || 1), scaleY: Math.max(0.05, Number(object.scaleY) || 1),
|
||||
@@ -677,6 +678,7 @@ function drawOrigin() {
|
||||
// 每个系统对应一个发射点(坐标点);没有系统时不画
|
||||
if (!store.systems.length) return
|
||||
for (const sys of store.systems) {
|
||||
if (sys.visible === false) continue
|
||||
const preview = emitterPreviewTransform.get(sys.id)
|
||||
const cx = preview?.x ?? sys.config.centerX ?? 0
|
||||
const cy = -(preview?.y ?? sys.config.centerY ?? 0)
|
||||
@@ -694,6 +696,44 @@ function drawOrigin() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 标出层级树当前选中的骨骼;发射器骨骼跟随复用同一目标点。 */
|
||||
function drawBoneFollowTargets() {
|
||||
if (!boneFollowTargetGfx) return
|
||||
const g = boneFollowTargetGfx
|
||||
g.parent?.addChild(g)
|
||||
g.clear()
|
||||
const targets = new Map<string, { spineId: number; boneName: string }>()
|
||||
for (const spine of store.spines) {
|
||||
if (spine.visible === false || !spine.selectedBone || spine.status !== 'ready') continue
|
||||
targets.set(`${spine.id}:${spine.selectedBone}`, { spineId: spine.id, boneName: spine.selectedBone })
|
||||
}
|
||||
for (const system of store.systems) {
|
||||
if (system.visible === false) continue
|
||||
const config = system.config
|
||||
if (!config.emitterFollow || config.emitterFollowMode !== 'bone') continue
|
||||
if (!config.emitterFollowSpineId || !config.emitterFollowBoneName) continue
|
||||
const spine = store.spines.find((item) => item.id === config.emitterFollowSpineId)
|
||||
if (!spine || spine.visible === false) continue
|
||||
targets.set(`${config.emitterFollowSpineId}:${config.emitterFollowBoneName}`, {
|
||||
spineId: config.emitterFollowSpineId,
|
||||
boneName: config.emitterFollowBoneName,
|
||||
})
|
||||
}
|
||||
for (const item of targets.values()) {
|
||||
const target = spineRuntime?.getBoneWorldTransform(item.spineId, item.boneName)
|
||||
if (!target) continue
|
||||
const x = target.x
|
||||
const y = -target.y
|
||||
g.lineStyle(2, 0xc48cff, 0.95)
|
||||
g.beginFill(0xc48cff, 0.2)
|
||||
g.drawCircle(x, y, 8)
|
||||
g.endFill()
|
||||
g.beginFill(0xf2e7ff, 1)
|
||||
g.drawCircle(x, y, 3.5)
|
||||
g.endFill()
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制当前选中系统的发射器形状范围(点/轨道不绘制),随参数实时响应
|
||||
function drawShapeRange() {
|
||||
if (!shapeGfx) return
|
||||
@@ -701,7 +741,7 @@ function drawShapeRange() {
|
||||
g.clear()
|
||||
if (!showEmitter.value) return
|
||||
const sys = store.systems.find((s) => s.id === store.activeId)
|
||||
if (!sys) return
|
||||
if (!sys || sys.visible === false) return
|
||||
const c = sys.config
|
||||
const preview = emitterPreviewTransform.get(sys.id)
|
||||
// root 中心(发射器容器 position = centerX/-centerY),数学坐标
|
||||
@@ -761,6 +801,7 @@ function drawForceField() {
|
||||
const g = forceFieldGfx
|
||||
g.clear()
|
||||
const sys = store.systems.find((item) => item.id === store.activeId)
|
||||
if (sys?.visible === false) return
|
||||
const cfg = sys?.config
|
||||
if (!cfg?.forceField || cfg.forceRadius <= 0) return
|
||||
const x = cfg.forceCenterX
|
||||
@@ -784,6 +825,7 @@ function drawAttraction() {
|
||||
const g = attractionGfx
|
||||
g.clear()
|
||||
const sys = store.systems.find((item) => item.id === store.activeId)
|
||||
if (sys?.visible === false) return
|
||||
const cfg = sys?.config
|
||||
if (!cfg?.attraction) return
|
||||
const x = cfg.attractionCenterX
|
||||
@@ -908,6 +950,7 @@ function drawCollisionBodies() {
|
||||
const g = collisionGfx
|
||||
g.clear()
|
||||
for (const collider of previewColliders) {
|
||||
if (collider.visible === false) continue
|
||||
const selected = store.activeObjectType === 'collision' && store.activeObjectId === collider.id
|
||||
const color = collider.enabled ? (selected ? 0xff5e72 : 0xd4475b) : 0x586174
|
||||
const angle = collider.rotation * Math.PI / 180
|
||||
@@ -954,7 +997,7 @@ function drawPaths() {
|
||||
const g = pathGfx
|
||||
g.clear()
|
||||
for (const path of store.paths) {
|
||||
if (!path.enabled || !path.points.length) continue
|
||||
if (path.visible === false || !path.enabled || !path.points.length) continue
|
||||
const selected = store.activeObjectType === 'path' && store.activeObjectId === path.id
|
||||
const color = colorNumber(path.color, 0xff933e)
|
||||
const screenPoint = (x: number, y: number) => {
|
||||
@@ -1072,9 +1115,13 @@ function buildPreviewColliders(timeSeconds: number) {
|
||||
})
|
||||
}
|
||||
|
||||
function worldPointToEmitterLocal(config: { centerX: number; centerY: number; rootRotation: number; rootScaleX: number; rootScaleY: number }, worldX: number, worldY: number) {
|
||||
const scaleX = Math.max(0.0001, Math.abs(config.rootScaleX))
|
||||
const scaleY = Math.max(0.0001, Math.abs(config.rootScaleY))
|
||||
function worldPointToEmitterLocal(
|
||||
config: { centerX: number; centerY: number; rootRotation: number; rootScaleX: number; rootScaleY: number },
|
||||
worldX: number,
|
||||
worldY: number,
|
||||
scaleX = Math.max(0.0001, Math.abs(config.rootScaleX)),
|
||||
scaleY = Math.max(0.0001, Math.abs(config.rootScaleY)),
|
||||
) {
|
||||
const angle = -config.rootRotation * Math.PI / 180
|
||||
const cos = Math.cos(angle), sin = Math.sin(angle)
|
||||
const screenX = worldX - config.centerX
|
||||
@@ -1096,16 +1143,29 @@ function loop() {
|
||||
drawAxis()
|
||||
syncEmitters()
|
||||
// 根节点(root)变换:位置(centerX/centerY)=位移,rotation/scale 绕 root 中心,与场景对象参数联动
|
||||
const followTime = store.timeline.frame / Math.max(1, store.timeline.fps)
|
||||
const followFps = Math.max(1, store.timeline.fps)
|
||||
const followFrame = store.timeline.frame
|
||||
const followTime = followFrame / followFps
|
||||
// 发射器读取骨骼变换前,先把 Spine 运行时同步到同一时间轴帧。
|
||||
spineRuntime?.sync(store.spines, followFrame, followFps)
|
||||
buildPreviewColliders(followTime)
|
||||
for (const sys of store.systems) {
|
||||
const em = emitterMap.get(sys.id)
|
||||
if (!em) continue
|
||||
const visible = sys.visible !== false
|
||||
em.visible = visible
|
||||
em.renderable = visible
|
||||
const c = sys.config
|
||||
em.scale.set(c.rootScaleX, c.rootScaleY)
|
||||
em.setColliders(previewColliders)
|
||||
em.setEmitterFollowSpawn(0, 0, 0)
|
||||
let preview = { x: c.centerX, y: c.centerY, rotation: c.rootRotation }
|
||||
const followSpine = c.emitterFollow && c.emitterFollowMode === 'bone'
|
||||
? store.spines.find((spine) => spine.id === c.emitterFollowSpineId && spine.enabled && spine.status === 'ready')
|
||||
: null
|
||||
const boneTransform = followSpine
|
||||
? spineRuntime?.getBoneWorldTransform(followSpine.id, c.emitterFollowBoneName) || null
|
||||
: null
|
||||
const followPath = c.emitterFollow && c.emitterFollowMode === 'path'
|
||||
? store.paths.find((path) => path.id === c.emitterFollowPathId && path.enabled)
|
||||
: null
|
||||
@@ -1117,7 +1177,33 @@ function loop() {
|
||||
? (1 - offset) * (1 - eased)
|
||||
: offset + (1 - offset) * eased
|
||||
const pathSample = followPath ? sampleScenePath(followPath, pathProgress) : null
|
||||
if (pathSample) {
|
||||
if (boneTransform) {
|
||||
// 骨骼只提供位置;旋转和缩放不传递给粒子,偏移使用世界坐标单位。
|
||||
const offsetX = c.emitterFollowBoneOffsetX
|
||||
const offsetY = c.emitterFollowBoneOffsetY
|
||||
const targetX = boneTransform.x + offsetX
|
||||
const targetY = boneTransform.y + offsetY
|
||||
const targetRotation = c.rootRotation + c.emitterFollowBoneRotation
|
||||
const targetScaleX = Math.max(0.0001, Math.abs(c.rootScaleX * c.emitterFollowBoneScaleX))
|
||||
const targetScaleY = Math.max(0.0001, Math.abs(c.rootScaleY * c.emitterFollowBoneScaleY))
|
||||
preview = { x: targetX, y: targetY, rotation: targetRotation }
|
||||
if (c.emitterFollowSpace === 'local') {
|
||||
// 本地空间:整个发射器容器随骨骼移动,已出生粒子也继承后续骨骼变换。
|
||||
em.position.set(targetX, -targetY)
|
||||
em.rotation = -targetRotation * Math.PI / 180
|
||||
em.scale.set(targetScaleX, targetScaleY)
|
||||
em.setSimulationTransform(targetX, targetY, targetRotation, targetScaleX, targetScaleY)
|
||||
} else {
|
||||
// 世界空间:保持粒子系统根容器不动,只把新粒子的出生点和方向移动到骨骼。
|
||||
em.position.set(c.centerX, -c.centerY)
|
||||
em.rotation = -c.rootRotation * Math.PI / 180
|
||||
em.scale.set(targetScaleX, targetScaleY)
|
||||
em.setSimulationTransform(c.centerX, c.centerY, c.rootRotation, targetScaleX, targetScaleY)
|
||||
const local = worldPointToEmitterLocal(c, targetX, targetY, targetScaleX, targetScaleY)
|
||||
const relativeDirection = -(targetRotation - c.rootRotation)
|
||||
em.setEmitterFollowSpawn(local.x, local.y, relativeDirection)
|
||||
}
|
||||
} else if (pathSample) {
|
||||
const movementAngle = pathSample.angle + (c.emitterFollowDirection === 'reverse' ? 180 : 0)
|
||||
const effectiveAngle = c.emitterAngleMode === 'direction' ? movementAngle : c.rootRotation
|
||||
preview = { x: pathSample.x, y: pathSample.y, rotation: effectiveAngle }
|
||||
@@ -1166,9 +1252,10 @@ function loop() {
|
||||
if (!tl.recorded) {
|
||||
// 录制阶段:固定步长模拟并记录当前帧。
|
||||
if (tl.frame === 0) { for (const [, em] of emitterMap) em.reset() }
|
||||
for (const [, em] of emitterMap) {
|
||||
for (const [systemId, em] of emitterMap) {
|
||||
const states = em.update(frameDuration)
|
||||
for (const state of states) if (state.active) appendStateBones(collected, state)
|
||||
const systemVisible = store.systems.find((item) => item.id === systemId)?.visible !== false
|
||||
if (systemVisible) for (const state of states) if (state.active) appendStateBones(collected, state)
|
||||
total += em.activeCount
|
||||
}
|
||||
for (const sys of store.systems) {
|
||||
@@ -1186,7 +1273,10 @@ function loop() {
|
||||
const frame = sys.frames?.[tl.frame]
|
||||
if (emitter && frame) {
|
||||
emitter.apply(frame)
|
||||
for (const state of frame) if (state.active) { appendStateBones(collected, state); total++ }
|
||||
for (const state of frame) if (state.active) {
|
||||
if (sys.visible !== false) appendStateBones(collected, state)
|
||||
total++
|
||||
}
|
||||
}
|
||||
}
|
||||
tl.frame++
|
||||
@@ -1205,19 +1295,23 @@ function loop() {
|
||||
if (!tl.playing && tl.recorded && emitter && frame) {
|
||||
// 暂停、逐帧或手动拖动播放头时立即显示指定的录制帧。
|
||||
emitter.apply(frame)
|
||||
for (const state of frame) if (state.active) { appendStateBones(collected, state); total++ }
|
||||
for (const state of frame) if (state.active) {
|
||||
if (sys.visible !== false) appendStateBones(collected, state)
|
||||
total++
|
||||
}
|
||||
} else if (emitter) {
|
||||
const states = emitter.update(0)
|
||||
for (const state of states) if (state.active) appendStateBones(collected, state)
|
||||
if (sys.visible !== false) for (const state of states) if (state.active) appendStateBones(collected, state)
|
||||
total += emitter.activeCount
|
||||
}
|
||||
}
|
||||
}
|
||||
timelineDisplayFrame = displayFrame
|
||||
spineRuntime?.sync(store.spines, displayFrame, fps, store.settings.showSpineResources)
|
||||
spineRuntime?.sync(store.spines, displayFrame, fps)
|
||||
for (const [, emitter] of emitterMap) emitter.drawTrailMeshDebug(showSkinMesh.value)
|
||||
drawCollisionBodies()
|
||||
drawPaths()
|
||||
drawBoneFollowTargets()
|
||||
drawOrigin()
|
||||
drawShapeRange()
|
||||
drawForceField()
|
||||
@@ -1315,6 +1409,9 @@ onUnmounted(() => {
|
||||
.tool-btn.on { background: #3a5a8c; color: #fff; border-color: #5a7ab8; }
|
||||
.zoom-pct { margin-left: auto; font-variant-numeric: tabular-nums; color: #9aa; min-width: 44px; text-align: center; }
|
||||
.hud { position: absolute; bottom: 12px; left: 12px; padding: 8px 12px; background: rgba(20,20,32,0.75); border-radius: 8px; font-size: 12px; color: #a0a8cc; }
|
||||
.hud-number { display: inline-block; box-sizing: border-box; text-align: right; font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }
|
||||
.hud-system-count { width: 3ch; }
|
||||
.hud-particle-count { width: 7ch; text-align: left; }
|
||||
.hud-tip { margin-left: 12px; color: #5f6a8a; font-size: 11px; }
|
||||
b { color: #ffcc33; }
|
||||
.settings-btn {
|
||||
|
||||
Reference in new issue
Block a user