轴点功能修复
This commit is contained in:
1 parent
4d54c87681
commit
2b8fd067b3
16 files changed
+1750
-105
No files matched your search
@@ -64,7 +64,7 @@
|
||||
<div class="pivot-toggle-sub" :title="pivotSummary">{{ pivotSummary }}</div>
|
||||
</div>
|
||||
<label class="pivot-switch" :title="pivotOpen ? '收起轴点调整' : '展开轴点调整'">
|
||||
<input v-model="pivotOpen" type="checkbox" />
|
||||
<input v-model="pivotOpen" type="checkbox" @change="onPivotToggle" />
|
||||
<span class="pivot-switch-ui"></span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -112,7 +112,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import NumSlider from './NumSlider.vue'
|
||||
import PivotEditor from './PivotEditor.vue'
|
||||
|
||||
@@ -138,6 +138,8 @@ const props = withDefaults(defineProps<{
|
||||
namePlaceholder?: string
|
||||
showFolder?: boolean
|
||||
supportsSequence?: boolean
|
||||
/** 资源卡片的稳定标识(如“系统id:资源id”),用于记住轴点面板展开状态。 */
|
||||
pivotKey?: string
|
||||
}>(), {
|
||||
namePlaceholder: 'particle',
|
||||
showFolder: true,
|
||||
@@ -169,17 +171,42 @@ const displayPreviewUrl = computed(() => props.resource.imageMode === 'sequence'
|
||||
? props.resource.sequenceFrames?.[0]?.previewUrl || ''
|
||||
: props.resource.previewUrl)
|
||||
|
||||
const pivotOpen = ref(false)
|
||||
const pivotFullscreen = ref(false)
|
||||
const draftX = ref(0.5)
|
||||
const draftY = ref(0.5)
|
||||
// 轴点面板展开状态属于 UI 状态:按资源独立记忆;用 sessionStorage 保证组件重挂载/HMR 后也不收起。
|
||||
const PIVOT_UI_STORAGE_KEY = 'SpineParticle.PivotPanels.v1'
|
||||
function loadPivotPanelState(): Record<string, boolean> {
|
||||
try {
|
||||
const parsed = JSON.parse(sessionStorage.getItem(PIVOT_UI_STORAGE_KEY) || '{}')
|
||||
return parsed && typeof parsed === 'object' ? parsed as Record<string, boolean> : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
const pivotOpenState = loadPivotPanelState()
|
||||
const resolvedPivotKey = computed(() => props.pivotKey || `particle-image:${props.resource.id}`)
|
||||
const supportsPivot = computed(() => typeof props.resource.anchorX === 'number' && typeof props.resource.anchorY === 'number')
|
||||
const pivotCentered = computed(() =>
|
||||
Math.abs((props.resource.anchorX ?? 0.5) - 0.5) < 1e-4 && Math.abs((props.resource.anchorY ?? 0.5) - 0.5) < 1e-4,
|
||||
)
|
||||
// 有明确记忆就用记忆;没有记忆时,已自定义轴点的资源默认展开,避免“开关关闭但轴点生效”的错觉。
|
||||
const hasStoredPivotOpen = typeof pivotOpenState[resolvedPivotKey.value] === 'boolean'
|
||||
const pivotOpen = ref(hasStoredPivotOpen ? pivotOpenState[resolvedPivotKey.value] : !pivotCentered.value)
|
||||
watch(pivotOpen, (open) => {
|
||||
pivotOpenState[resolvedPivotKey.value] = open
|
||||
try { sessionStorage.setItem(PIVOT_UI_STORAGE_KEY, JSON.stringify(pivotOpenState)) } catch { /* 隐私模式忽略 */ }
|
||||
})
|
||||
watch(resolvedPivotKey, (key) => {
|
||||
pivotOpen.value = typeof pivotOpenState[key] === 'boolean' ? pivotOpenState[key] : !pivotCentered.value
|
||||
})
|
||||
// 轴点已自定义时开关必须为开启状态,避免出现“开关关闭但轴点仍生效”的不一致。
|
||||
watch(() => pivotCentered.value, (centered) => {
|
||||
if (!centered) pivotOpen.value = true
|
||||
}, { immediate: true })
|
||||
const pivotFullscreen = ref(false)
|
||||
const draftX = ref(0.5)
|
||||
const draftY = ref(0.5)
|
||||
const pivotSummary = computed(() => {
|
||||
const x = (Math.min(1, Math.max(0, props.resource.anchorX ?? 0.5)) * 100).toFixed(1)
|
||||
const y = (Math.min(1, Math.max(0, props.resource.anchorY ?? 0.5)) * 100).toFixed(1)
|
||||
const x = ((props.resource.anchorX ?? 0.5) * 100).toFixed(1)
|
||||
const y = ((props.resource.anchorY ?? 0.5) * 100).toFixed(1)
|
||||
if (pivotOpen.value) return `已开启:X ${x}% | Y ${y}%`
|
||||
if (pivotCentered.value) return '未开启,默认围绕图片正中心 (50%, 50%)'
|
||||
return `轴点已自定义:X ${x}% | Y ${y}%`
|
||||
@@ -191,6 +218,14 @@ function openPivotFullscreen() {
|
||||
pivotFullscreen.value = true
|
||||
}
|
||||
|
||||
/** 关闭开关表示回到默认中心轴点;自定义轴点不允许处于“关闭”状态。 */
|
||||
function onPivotToggle() {
|
||||
if (pivotOpen.value) return
|
||||
props.resource.anchorX = 0.5
|
||||
props.resource.anchorY = 0.5
|
||||
pivotFullscreen.value = false
|
||||
}
|
||||
|
||||
function closePivotFullscreen(commit: boolean) {
|
||||
if (commit) {
|
||||
props.resource.anchorX = draftX.value
|
||||
|
||||
+17
-13
@@ -55,11 +55,11 @@
|
||||
<div class="pivot-card-head"><span><span class="pivot-card-icon">◎</span> 轴点坐标(%)</span></div>
|
||||
<div class="pivot-coords">
|
||||
<label>X:
|
||||
<input type="number" min="0" max="100" step="0.1" :value="anchorXText" @input="updateCoord('x', $event)" />
|
||||
<input type="number" min="-1000" max="1000" step="0.1" :value="anchorXText" @input="updateCoord('x', $event)" />
|
||||
<span>%</span>
|
||||
</label>
|
||||
<label>Y:
|
||||
<input type="number" min="0" max="100" step="0.1" :value="anchorYText" @input="updateCoord('y', $event)" />
|
||||
<input type="number" min="-1000" max="1000" step="0.1" :value="anchorYText" @input="updateCoord('y', $event)" />
|
||||
<span>%</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -133,9 +133,12 @@ const rotationMode = ref<'static' | 'spin' | 'swing'>('static')
|
||||
|
||||
let observer: ResizeObserver | null = null
|
||||
|
||||
const clampPercent = (value: number) => Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0))
|
||||
const anchorXText = computed(() => (clampPercent(props.anchorX * 100)).toFixed(1))
|
||||
const anchorYText = computed(() => (clampPercent(props.anchorY * 100)).toFixed(1))
|
||||
// 轴点允许落在图片之外;这里只做 ±1000% 的防护上限。
|
||||
const COORD_LIMIT_PERCENT = 1000
|
||||
const clampCoordPercent = (value: number) =>
|
||||
Math.min(COORD_LIMIT_PERCENT, Math.max(-COORD_LIMIT_PERCENT, Number.isFinite(value) ? value : 0))
|
||||
const anchorXText = computed(() => clampCoordPercent(props.anchorX * 100).toFixed(1))
|
||||
const anchorYText = computed(() => clampCoordPercent(props.anchorY * 100).toFixed(1))
|
||||
|
||||
function contain(width: number, height: number) {
|
||||
const ratio = natural.value.w / Math.max(1, natural.value.h)
|
||||
@@ -162,8 +165,8 @@ const boxStyle = computed(() => ({
|
||||
const pivotScreen = computed(() => {
|
||||
const scale = zoom.value / 100
|
||||
return {
|
||||
x: stageSize.value.w / 2 + pan.value.x + (clampPercent(props.anchorX * 100) / 100 - 0.5) * fit.value.w * scale,
|
||||
y: stageSize.value.h / 2 + pan.value.y + (clampPercent(props.anchorY * 100) / 100 - 0.5) * fit.value.h * scale,
|
||||
x: stageSize.value.w / 2 + pan.value.x + (clampCoordPercent(props.anchorX * 100) / 100 - 0.5) * fit.value.w * scale,
|
||||
y: stageSize.value.h / 2 + pan.value.y + (clampCoordPercent(props.anchorY * 100) / 100 - 0.5) * fit.value.h * scale,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -173,13 +176,14 @@ const rotateStyle = computed(() => {
|
||||
return {
|
||||
width: `${box.w}px`,
|
||||
height: `${box.h}px`,
|
||||
transformOrigin: `${clampPercent(props.anchorX * 100)}% ${clampPercent(props.anchorY * 100)}%`,
|
||||
transformOrigin: `${clampCoordPercent(props.anchorX * 100)}% ${clampCoordPercent(props.anchorY * 100)}%`,
|
||||
}
|
||||
})
|
||||
|
||||
function setAnchor(x: number, y: number) {
|
||||
emit('update:anchorX', Math.min(1, Math.max(0, Number.isFinite(x) ? x : 0)))
|
||||
emit('update:anchorY', Math.min(1, Math.max(0, Number.isFinite(y) ? y : 0)))
|
||||
const limit = COORD_LIMIT_PERCENT / 100
|
||||
emit('update:anchorX', Math.min(limit, Math.max(-limit, Number.isFinite(x) ? x : 0)))
|
||||
emit('update:anchorY', Math.min(limit, Math.max(-limit, Number.isFinite(y) ? y : 0)))
|
||||
}
|
||||
|
||||
function updateFromPointer(event: PointerEvent) {
|
||||
@@ -275,12 +279,12 @@ function pickPreset(preset: { x: number; y: number }) {
|
||||
}
|
||||
|
||||
function isPreset(preset: { x: number; y: number }) {
|
||||
return Math.abs(clampPercent(props.anchorX * 100) - preset.x) < 0.05
|
||||
&& Math.abs(clampPercent(props.anchorY * 100) - preset.y) < 0.05
|
||||
return Math.abs(clampCoordPercent(props.anchorX * 100) - preset.x) < 0.05
|
||||
&& Math.abs(clampCoordPercent(props.anchorY * 100) - preset.y) < 0.05
|
||||
}
|
||||
|
||||
function updateCoord(axis: 'x' | 'y', event: Event) {
|
||||
const value = clampPercent(Number((event.target as HTMLInputElement).value))
|
||||
const value = clampCoordPercent(Number((event.target as HTMLInputElement).value))
|
||||
if (axis === 'x') emit('update:anchorX', value / 100)
|
||||
else emit('update:anchorY', value / 100)
|
||||
}
|
||||
|
||||
@@ -87,9 +87,10 @@
|
||||
</div>
|
||||
<ImageResourceCard
|
||||
v-for="resource in sys.config.imageResources"
|
||||
:key="resource.id"
|
||||
:key="`${sys.id}:${resource.id}`"
|
||||
:resource="resource"
|
||||
:resource-count="sys.config.imageResources.length"
|
||||
:pivot-key="`${sys.id}:${resource.id}`"
|
||||
supports-sequence
|
||||
default-file="star.png"
|
||||
@pick="openTexturePicker"
|
||||
|
||||
Reference in new issue
Block a user