Improve Android app UI and controller flows

This commit is contained in:
2026-10-09 16:43:58 +08:00
parent c0e26b97fe
commit 608229a497
90 changed files with 8103 additions and 1133 deletions
@@ -1,3 +1,4 @@
import { getSelectedLayer, boardExtent, editSnapshot, rememberEdit, editHistorySize, clearEditHistory, undoSceneEdit, setExpandedBoard, setMoveFrameVisible, moveSelectedLayer, previewLayerMove, cancelLayerPreview, clearPixelLayer, subscribeScene } from "../scene-board.js";
import {
announce,
createLocalState,
@@ -81,7 +82,13 @@ let suppressAssistiveClick = false;
let coordinateGuideFrame = null;
let assistivePosition = { x: 36, y: window.innerHeight / 2 };
const viewPointers = new Map();
const undoHistory = new PixelUndoHistory();
let expandedEnabled=Boolean(localState.load({}).expandedEnabled);
let layerDrag=null;
const expandedToggle=document.getElementById("toggle-expanded-board");
const moveLayerButton=document.getElementById("move-current-layer");
const exitLayerMoveButton=document.getElementById("exit-layer-move");
const moveControls=document.getElementById("layer-move-controls");
const offsetX=document.getElementById("layer-offset-x"),offsetY=document.getElementById("layer-offset-y");
paintColor.value = savedTools.paintColor || paintColor.value;
backgroundColor.value = savedTools.backgroundColor || backgroundColor.value;
@@ -94,21 +101,19 @@ function currentBrushSize() {
return pixelEditEnabled ? pixelBrushSize : normalBrushSize;
}
function setPixel(x, y, rgb) {
if (x < 0 || x > 63 || y < 0 || y > 63) return;
const offset = (y * 64 + x) * 3;
const pixels = getPixelBuffer();
pixels[offset] = rgb.r;
pixels[offset + 1] = rgb.g;
pixels[offset + 2] = rgb.b;
function setPixel(x,y,rgb) {
const layer=getSelectedLayer(),{origin,size}=boardExtent(),worldX=x-64+layer.x,worldY=y-64+layer.y;
if(x<0||x>=192||y<0||y>=192||worldX<origin||worldX>=origin+size||worldY<origin||worldY>=origin+size)return;
const i=(y*192+x)*4;
layer.pixels.set(tool==="eraser"?[0,0,0,0]:[rgb.r,rgb.g,rgb.b,255],i);
}
function activeRgb() {
return tool === "eraser" ? hexToRgb(backgroundColor.value) : hexToRgb(paintColor.value);
return hexToRgb(paintColor.value);
}
function stampBrush(centerX, centerY, rgb) {
brushStampPoints(centerX, centerY, currentBrushSize(), brushMode())
brushStampPoints(centerX+64-getSelectedLayer().x, centerY+64-getSelectedLayer().y, currentBrushSize(), brushMode(),192,192)
.forEach((point) => setPixel(point.x, point.y, rgb));
}
@@ -127,24 +132,14 @@ function paintEvent(event) {
renderPixelDraft();
}
function pixelSnapshot() {
return Uint8Array.from(getPixelBuffer());
}
function updateUndoControl() {
if (undoHistory.size > 0) setControlReady(undoButton);
else setControlUnavailable(undoButton, "还没有可以撤销的像素操作。");
}
function clearUndoHistory() {
undoHistory.clear();
updateUndoControl();
}
function pixelSnapshot() {return {scene:editSnapshot(),pixels:getSelectedLayer().pixels?.slice()};}
function updateUndoControl() {if(editHistorySize()>0)setControlReady(undoButton);else setControlUnavailable(undoButton,"还没有可以撤销的编辑操作。");}
function clearUndoHistory() {clearEditHistory();updateUndoControl();}
function recordPixelOperation(before) {
const recorded = undoHistory.record(before, getPixelBuffer());
updateUndoControl();
return recorded;
if(!before)return false;
const l=getSelectedLayer(),changed=before.pixels?.some((v,i)=>v!==l.pixels[i]);
if(changed)l.revision++;
if(before.scene!==editSnapshot()){rememberEdit(before.scene);updateUndoControl();return true;}return false;
}
function viewportBounds() {
@@ -191,8 +186,10 @@ function renderCoordinateGuide() {
const bottom = Math.min(height, rect.bottom);
if (right <= left || bottom <= top || rect.width <= 0) return;
const step = chooseCoordinateGuideStep(rect.width / 64);
const extent=boardExtent();
const step = chooseCoordinateGuideStep(rect.width / extent.size);
const xTicks = coordinateGuideTicks({
count: extent.size, origin: extent.origin,
boardStart: rect.left,
boardSize: rect.width,
visibleStart: left,
@@ -200,6 +197,7 @@ function renderCoordinateGuide() {
step,
});
const yTicks = coordinateGuideTicks({
count: extent.size, origin: extent.origin,
boardStart: rect.top,
boardSize: rect.height,
visibleStart: top,
@@ -296,6 +294,8 @@ function applyAssistivePosition(next = assistivePosition) {
});
assistive.style.setProperty("--assistive-x", `${assistivePosition.x}px`);
assistive.style.setProperty("--assistive-y", `${assistivePosition.y}px`);
const room=assistivePosition.y>window.innerHeight/2?assistivePosition.y:window.innerHeight-assistivePosition.y;
assistive.style.setProperty("--assistive-menu-height",`${Math.max(120,room+16)}px`);
assistive.dataset.menuHorizontal = assistivePosition.x > window.innerWidth / 2 ? "left" : "right";
assistive.dataset.menuVertical = assistivePosition.y > window.innerHeight / 2 ? "up" : "down";
}
@@ -327,17 +327,20 @@ function toggleAssistiveMenu() {
function updateEditorModeControls() {
const moving = editorMode === "move";
const textEditing = getSelectedLayer().type === "text";
moveLayerButton.setAttribute("aria-pressed",String(editorMode==="layer"));
if (editorActive) document.body.dataset.canvasEditorMode = editorMode;
else delete document.body.dataset.canvasEditorMode;
editorModeHint.textContent = moving
editorModeHint.textContent = editorMode==="layer"?"移动图层:拖动或使用偏移坐标;黄色框为实际屏幕":moving
? "移动模式:单指拖动画布,双指捏合缩放;桌面可使用滚轮缩放。"
: "绘画模式:直接在画布上绘制。";
: textEditing ? "文字编辑模式:拖动文字或使用缩放手柄。" : "绘画模式:直接在画布上绘制。";
if (moving) {
setControlUnavailable(moveViewButton, "当前已经是移动画布视线模式。");
setControlReady(continueEditButton);
} else {
setControlReady(moveViewButton);
setControlUnavailable(continueEditButton, "当前已经是绘画模式。");
if (editorMode === "layer") setControlReady(continueEditButton);
else setControlUnavailable(continueEditButton, textEditing ? "当前已经是文字编辑模式。" : "当前已经是绘画模式。");
}
updateUndoControl();
}
@@ -361,24 +364,36 @@ function resetViewGesture() {
function setEditorMode(mode) {
if (!editorActive) return;
if (mode === "move") finishDrawing();
editorMode = mode === "move" ? "move" : "draw";
const pointerIds = [...viewPointers.keys()];
if (layerDrag) {
pointerIds.push(layerDrag.id);
layerDrag = null;
cancelLayerPreview();
}
viewPointers.clear();
viewGesture = null;
for (const id of pointerIds) {
if (canvas.hasPointerCapture(id)) canvas.releasePointerCapture(id);
}
if (mode === "move") finishDrawing();
editorMode = ["move","layer"].includes(mode)?mode:"draw";
if (editorMode === "draw") moveControls.hidden = true;
setMoveFrameVisible(editorMode==="layer" || !moveControls.hidden);
updateEditorModeControls();
closeAssistiveMenu({ restoreFocus: true });
announce(editorMode === "move"
? "已进入移动画布视线模式:单指拖动,双指缩放"
: "已继续编辑画布");
: editorMode === "layer" ? "已进入移动当前图层模式:拖动图层或输入偏移" : "已继续编辑画布");
}
function enterCanvasEditor() {
if (editorActive || getInteractionMode() !== "pixels") return;
if (editorActive || getInteractionMode() === "readonly") return;
editorActive = true;
editorMode = "draw";
canvas.dataset.canvasEditing = "true";
document.body.classList.add("canvas-edit-active");
assistive.hidden = false;
setExpandedBoard(expandedEnabled);
closeAssistiveMenu();
updateEditorModeControls();
updateCoordinateGuideControl();
@@ -400,6 +415,7 @@ function exitCanvasEditor({ returnToTools = true } = {}) {
viewGesture = null;
closeAssistiveMenu();
assistive.hidden = true;
moveControls.hidden=true;setMoveFrameVisible(false);setExpandedBoard(false);cancelLayerPreview();layerDrag=null;
hideCoordinateGuide();
delete canvas.dataset.canvasEditing;
document.body.classList.remove("canvas-edit-active");
@@ -417,17 +433,7 @@ function exitCanvasEditor({ returnToTools = true } = {}) {
announce("已退出画布编辑,可以重新选择颜色和画笔工具");
}
function undoPixelOperation() {
const previous = undoHistory.undo();
if (!previous) {
updateUndoControl();
return;
}
getPixelBuffer().set(previous);
commitPixelChanges("pixel-undo");
updateUndoControl();
announce("已撤销上一个像素操作");
}
function undoPixelOperation() {if(undoSceneEdit())announce("已撤销上一个编辑操作");updateUndoControl();}
function persistTools() {
localState.save({
@@ -438,6 +444,7 @@ function persistTools() {
normalBrushSize,
pixelBrushSize,
coordinateGuideEnabled,
expandedEnabled,
});
}
@@ -516,7 +523,10 @@ function selectTool(button) {
toolButtons.forEach((button) => button.addEventListener("click", () => selectTool(button)));
canvas.addEventListener("pointerdown", (event) => {
if (!editorActive || getInteractionMode() !== "pixels") return;
if (!editorActive || getInteractionMode() === "readonly") return;
if (editorMode === "layer") {
event.preventDefault();const l=getSelectedLayer();layerDrag={id:event.pointerId,start:boardPoint(event,{round:false}),x:l.x,y:l.y};canvas.setPointerCapture(event.pointerId);return;
}
if (editorMode === "move") {
event.preventDefault();
viewPointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
@@ -524,10 +534,11 @@ canvas.addEventListener("pointerdown", (event) => {
resetViewGesture();
return;
}
if (drawing) return;
if (getInteractionMode()!=="pixels" || drawing) return;
if (tool === "eyedropper") {
const point = boardPoint(event);
const offset = (point.y * 64 + point.x) * 3;
const l=getSelectedLayer();
const offset = ((point.y+64-l.y)*192+point.x+64-l.x)*4;
const pixels = getPixelBuffer();
setColorValue(paintColor, rgbToHex({
r: pixels[offset],
@@ -550,6 +561,7 @@ canvas.addEventListener("pointerdown", (event) => {
});
canvas.addEventListener("pointermove", (event) => {
if(layerDrag?.id===event.pointerId) {const p=boardPoint(event,{round:false});previewLayerMove(layerDrag.x+p.x-layerDrag.start.x,layerDrag.y+p.y-layerDrag.start.y);return;}
if (editorActive && editorMode === "move" && viewPointers.has(event.pointerId)) {
event.preventDefault();
viewPointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
@@ -594,6 +606,12 @@ function finishDrawing() {
}
function finishCanvasPointer(event) {
if(layerDrag?.id===event.pointerId) {
const drag=layerDrag;layerDrag=null;
if(event.type==="pointercancel"||event.type==="lostpointercapture")cancelLayerPreview();
else {const p=boardPoint(event,{round:false});try{moveSelectedLayer(drag.x+p.x-drag.start.x,drag.y+p.y-drag.start.y);}catch(e){cancelLayerPreview();announce(e.message,true);}}
syncOffsets();return;
}
if (viewPointers.delete(event.pointerId)) {
resetViewGesture();
return;
@@ -625,13 +643,13 @@ document.getElementById("fill-canvas").addEventListener("click", () => {
const rgb = hexToRgb(backgroundColor.value);
fillPixelLayer([rgb.r, rgb.g, rgb.b]);
recordPixelOperation(before);
announce("背景颜色已填充到像素层,文字元素保持不变");
announce("已开启当前画笔图层的背景");
});
document.getElementById("clear-canvas").addEventListener("click", () => {
const before = pixelSnapshot();
fillPixelLayer([0, 0, 0]);
clearPixelLayer();
recordPixelOperation(before);
announce("像素层已清空,文字元素保持不变");
announce("当前图层笔迹已清空,背景保持不变");
});
startEditButton.addEventListener("click", enterCanvasEditor);
@@ -639,6 +657,7 @@ undoButton.addEventListener("click", undoPixelOperation);
exitEditButton.addEventListener("click", () => exitCanvasEditor());
moveViewButton.addEventListener("click", () => setEditorMode("move"));
continueEditButton.addEventListener("click", () => setEditorMode("draw"));
exitLayerMoveButton.addEventListener("click", () => setEditorMode("draw"));
assistiveToggle.addEventListener("pointerdown", (event) => {
if (!editorActive) return;
@@ -728,3 +747,20 @@ registerWorkspace({
hideCoordinateGuide();
},
});
function syncOffsets(){const l=getSelectedLayer();offsetX.value=l.x;offsetY.value=l.y;}
function commitOffsets(){try{moveSelectedLayer(Number(offsetX.value),Number(offsetY.value));}catch(e){announce(e.message,true);}syncOffsets();}
expandedToggle.setAttribute("aria-pressed",String(expandedEnabled));
expandedToggle.textContent=expandedEnabled?"隐藏扩展画面":"显示扩展画面";
expandedToggle.addEventListener("click",()=>{finishDrawing();expandedEnabled=!expandedEnabled;setExpandedBoard(expandedEnabled);expandedToggle.setAttribute("aria-pressed",String(expandedEnabled));expandedToggle.textContent=expandedEnabled?"隐藏扩展画面":"显示扩展画面";persistTools();updatePixelGridVisibility();applyCanvasView();});
moveLayerButton.addEventListener("click",()=>{finishDrawing();moveControls.hidden=false;syncOffsets();setEditorMode("layer");openAssistiveMenu();});
for(const input of [offsetX,offsetY]){
input.addEventListener("change",commitOffsets);
input.addEventListener("keydown",e=>{if(e.key==="Enter"){e.preventDefault();input.blur();}});
}
for(const button of document.querySelectorAll("[data-offset]"))button.addEventListener("click",()=>{
const l=getSelectedLayer(),axis=button.parentElement.dataset.axis,delta=Number(button.dataset.offset);
try{moveSelectedLayer(l.x+(axis==="x"?delta:0),l.y+(axis==="y"?delta:0));}catch(e){announce(e.message,true);}syncOffsets();
});
document.addEventListener("matrix:edit-history",updateUndoControl);
subscribeScene(({reason})=>{if(["selection","layer-move","undo","move-cancel"].includes(reason)){syncOffsets();updatePixelGridVisibility();}});
@@ -0,0 +1,121 @@
import { announce, apiJson } from "../core.js";
import { runControlAction, setControlReady, setControlUnavailable } from "../interaction-feedback.js";
import { layerToStorage, sceneToStorageValue } from "../scene-model.js";
import { getScene, getSelectedLayer, subscribeScene, selectLayer, changeLayer, setLayerBackground,
animationEditState, templateEditState, enterTemplateEdit, enterAnimationEdit, markTemplateSaved,
markAnimationFrameSaved } from "../scene-board.js";
import { initializeTextWorkspace } from "./text.js";
import { refreshTemplates } from "./templates.js";
import { refreshAnimations } from "./animations.js";
const byId=id=>document.getElementById(id);
const list=byId("layer-list"), background=byId("layer-background-enabled"), color=byId("background-color");
const dialog=byId("layer-copy-dialog"),target=byId("layer-copy-target"),feedback=byId("layer-copy-feedback");
const submit=byId("layer-copy-submit"),frames=byId("layer-copy-frame-list");
let source=null,targets=new Map(),targetDetail=null,loadSequence=0,busy=false;
function render({scene}) {
const selected=getSelectedLayer(),brush=selected.type==="brush";
byId("brush-layer-tools").hidden=!brush;byId("text-layer-tools").hidden=brush;
background.checked=Boolean(selected.background);
if(brush){color.value=selected.backgroundColor||selected.background||"#000000";color.dispatchEvent(new Event("input"));}
list.replaceChildren(...[...scene.layers].reverse().map(l=>{
const button=document.createElement("button");button.type="button";button.className="layer-item";
button.textContent=`${l.name} · ${l.type==="brush"?"画笔":"文字"}`;button.setAttribute("aria-pressed",String(l.id===selected.id));
button.addEventListener("click",()=>selectLayer(l.id));return button;
}));
const index=scene.layers.indexOf(selected);
for(const [id,allowed,reason] of [["delete-layer",!brush||scene.layers.filter(l=>l.type==="brush").length>1,"必须保留一个画笔图层。"],
["raise-layer",index<scene.layers.length-1,"已经在最上方。"],["lower-layer",index>0,"已经在最下方。"]]) {
if(allowed)setControlReady(byId(id));else setControlUnavailable(byId(id),reason);
}
if(!brush)initializeTextWorkspace().catch(e=>announce(e.message,true));
}
subscribeScene(render);
for(const [id,action,value] of [["new-brush-layer","create","brush"],["new-text-layer","create","text"],
["delete-layer","delete"],["raise-layer","up"],["lower-layer","down"]])byId(id).addEventListener("click",()=>changeLayer(action,value));
byId("rename-layer").addEventListener("click",()=>{
const name=window.prompt("图层名称(1..80个字符)",getSelectedLayer().name);
if(name!==null)try{changeLayer("rename",name);}catch(e){announce(e.message,true);}
});
background.addEventListener("change",()=>setLayerBackground(background.checked,color.value));
color.addEventListener("input",()=>{if(getSelectedLayer().type==="brush"&&getSelectedLayer().backgroundColor!==color.value)setLayerBackground(background.checked,color.value);});
function dirtyChoice(){
const d=document.createElement("dialog");d.className="layer-copy-dialog";
const title=document.createElement("h3");title.textContent="目标画面正在编辑,存在未保存修改";d.append(title);
for(const [value,label] of [["save","保存后复制"],["discard","放弃修改后复制"],["cancel","取消"]]){
const b=document.createElement("button");b.type="button";b.textContent=label;b.addEventListener("click",()=>d.close(value));d.append(b);
}
document.body.append(d);d.showModal();return new Promise(resolve=>d.addEventListener("close",()=>{const choice=d.returnValue||"cancel";d.remove();resolve(choice);},{once:true}));
}
async function loadFrames(){
const sequence=++loadSequence;targetDetail=null;frames.replaceChildren();byId("layer-copy-frames").hidden=true;
const ref=targets.get(target.value);
if(!ref){setControlReady(submit);return;}
setControlUnavailable(submit,"正在读取目标帧。");feedback.textContent="正在读取目标帧…";
try{
const detail=await apiJson(ref.type==="template"?`/api/templates/${ref.id}`:`/api/animations/${ref.id}/frames`);if(sequence!==loadSequence)return;
targetDetail=detail;
if(ref.type==="template"){feedback.textContent="副本默认置顶。";setControlReady(submit);return;}
byId("layer-copy-frames").hidden=false;
for(const [i,frame] of detail.frames.entries()) {
const label=document.createElement("label"),input=document.createElement("input");input.type="checkbox";input.value=frame.id;
const name=document.createElement("span");name.textContent=`${i+1}. ${frame.name||"未命名帧"}`;
label.append(input,name);frames.append(label);
}
feedback.textContent=detail.frames.length?"选择一个或多个目标帧,复制后置顶。":"目标动图没有帧,请先添加帧。";
if(detail.frames.length)setControlReady(submit);else setControlUnavailable(submit,"目标动图没有帧。");
}catch(e){if(sequence===loadSequence){feedback.textContent=e.message;setControlUnavailable(submit,"读取失败,请重新选择目标。");}}
}
byId("copy-layer").addEventListener("click",async()=>{
const sequence=++loadSequence;
source=layerToStorage(getSelectedLayer());targets=new Map();target.replaceChildren(new Option("当前画板","current"));
feedback.textContent="正在读取可复制目标…";dialog.showModal();setControlUnavailable(submit,"正在读取目标。");
try{
const [templates,animations]=await Promise.all([apiJson("/api/templates"),apiJson("/api/animations")]);
if(sequence!==loadSequence)return;
for(const [type,items] of [["template",templates.templates],["animation",animations.animations]])for(const item of items.filter(i=>!i.is_demo&&!i.read_only)){
const key=`${type}:${item.id}`;targets.set(key,{...item,type});target.append(new Option(`${type==="template"?"静态":"动图"} · ${item.name}`,key));
}
feedback.textContent="副本默认置顶,不改变原图层。";await loadFrames();
}catch(e){feedback.textContent=e.message;setControlReady(submit);}
});
target.addEventListener("change",loadFrames);
for(const [id,checked] of [["layer-copy-all",true],["layer-copy-none",false]])byId(id).addEventListener("click",()=>frames.querySelectorAll("input").forEach(input=>input.checked=checked));
byId("layer-copy-cancel").addEventListener("click",()=>{if(!busy){loadSequence++;dialog.close();}});
dialog.addEventListener("cancel",e=>{if(busy)e.preventDefault();else loadSequence++;});
submit.addEventListener("click",()=>runControlAction(submit,{message:"正在复制图层…"},async()=>{
busy=true;target.disabled=true;frames.inert=true;
try{
if(target.value==="current"){changeLayer("duplicate");dialog.close();announce("图层已复制到当前画板最上方");return;}
const ref=targets.get(target.value);if(!ref)throw Error("请选择有效目标");
const edit=ref.type==="template"?templateEditState():animationEditState();
// An ordinary template parked behind animation editing is not the active board.
const editing=edit&&(ref.type==="template"?edit.id===ref.id&&!animationEditState():edit.animationId===ref.id);
let savedRevision=null;
if(editing&&edit.dirty){
const choice=await dirtyChoice();if(choice==="cancel")return;
if(choice==="save"){
const path=ref.type==="template"?`/api/templates/${ref.id}`:`/api/animations/${ref.id}/frames/${edit.frameId}`;
const saved=await apiJson(path,{method:"PUT",headers:{"If-Match":`"${edit.revision}"`},body:JSON.stringify({scene:sceneToStorageValue(getScene())})});
if(ref.type==="template")markTemplateSaved(saved);else markAnimationFrameSaved(saved);
savedRevision=saved.revision;
}
}
const current=targetDetail;
if(!current)throw Error("目标状态已失效,请重新选择");
// Preserve the revision from frame selection: intervening writes must conflict.
const frameIds=Array.from(frames.querySelectorAll("input:checked"),input=>input.value);
if(ref.type==="animation"&&!frameIds.length)throw Error("请至少选择一个目标帧");
const revision=savedRevision||current.revision;
const result=await apiJson(ref.type==="template"?`/api/templates/${ref.id}/layers`:`/api/animations/${ref.id}/frames/layers`,{
method:"POST",headers:{"If-Match":`"${revision}"`},body:JSON.stringify({layer:source,...(ref.type==="animation"?{frame_ids:frameIds}:{})})});
if(editing){if(ref.type==="template")enterTemplateEdit(result);else{
const frame=await apiJson(`/api/animations/${ref.id}/frames/${edit.frameId}`);
enterAnimationEdit({...frame,animation_id:ref.id,animation_name:result.name,animation_revision:result.revision});
}}
dialog.close();announce("图层已复制到目标最上方");
await Promise.all([refreshTemplates(),refreshAnimations()]);
}catch(e){feedback.textContent=`复制失败:${e.message}`;announce(feedback.textContent,true);}
finally{busy=false;target.disabled=false;frames.inert=false;}
}));
@@ -528,7 +528,7 @@ export async function refreshTemplates() {
if (!userItems.length) {
const empty = document.createElement("p");
empty.className = "template-empty panel";
empty.textContent = "还没有模板。请在像素画布或文字显示中保存当前统一画板。";
empty.textContent = "还没有模板。请在画图页保存当前统一画板。";
grid.appendChild(empty);
}
feedback.textContent = `共 ${data.templates.length} 个静态模板、${animationList.length} 个动图`;
@@ -12,6 +12,7 @@ import {
deleteTextElement,
duplicateTextElement,
getScene,
getSelectedLayer,
getSelectedTextElement,
selectTextElement,
subscribeScene,
@@ -116,7 +117,7 @@ function displayName(element, index) {
}
function renderElementList(scene, selectedId) {
const items = scene.elements.map((element, index) => {
const items = (getSelectedLayer().elements||[]).map((element, index) => {
const button = document.createElement("button");
button.type = "button";
button.className = "text-element-item";
@@ -144,7 +145,7 @@ function renderSelection({ scene, selectedTextId, reason }) {
selectionStatus.textContent = selected
? `已选中“${selected.text || "空文字"}” · 可在画板拖动,拖右下角控制点等比缩放`
: "请先新建或选择一个文字元素;也可点击画板中文字或从列表选择。";
const shouldRefreshFields = renderedSelectionId !== selectedTextId
const shouldRefreshFields = reason === "selection" || reason === "undo" || reason === "move-cancel" || reason === "layer-move" || renderedSelectionId !== selectedTextId
|| ["subscribe", "selection", "create", "duplicate", "delete", "gesture"].includes(reason);
if (shouldRefreshFields) applyElementToFields(selected);
}
@@ -169,12 +170,28 @@ function formPatch(element) {
form.addEventListener("input", (event) => {
if (event.target === fontInput || event.target === fontFileInput) return;
if (["text-x", "text-y"].includes(event.target.id)) return;
if (suppressForm) return;
const selected = getSelectedTextElement();
if (!selected) return;
updateTextElement(selected.id, formPatch(selected), "form");
const patch = formPatch(selected);
delete patch.x;
delete patch.y;
updateTextElement(selected.id, patch, "form");
});
for (const id of ["text-x", "text-y"]) {
field(id).addEventListener("change", () => {
const selected = getSelectedTextElement();
if (selected) updateTextElement(selected.id, {
x: numericValue("text-x", selected.x), y: numericValue("text-y", selected.y),
}, "gesture");
});
field(id).addEventListener("keydown", event => {
if (event.key === "Enter") { event.preventDefault(); field(id).blur(); }
});
}
document.getElementById("create-text").addEventListener("click", () => {
const created = addTextElement({
text: "ok",
@@ -332,18 +349,10 @@ async function initializeTextWorkspace() {
}
}
if (!getSelectedTextElement()) {
const topmost = getScene().elements.at(-1);
const topmost = getSelectedLayer().elements?.at(-1);
if (topmost) selectTextElement(topmost.id);
else applyElementToFields(null);
}
}
registerWorkspace({
id: "text",
title: "文字显示",
elementId: "workspace-text",
order: 40,
persistence: "local",
capabilities: ["scene-template-save"],
onEnter: initializeTextWorkspace,
});
export { initializeTextWorkspace };