import { announce, createLocalState, registerWorkspace, setActiveButton, } from "../core.js"; import { refreshColorTrigger, setColorValue } from "../color-picker.js"; import { hexToRgb, rgbToHex } from "../color-math.js"; import { BRUSH_MODE_NORMAL, BRUSH_MODE_PIXEL, PixelUndoHistory, brushModeSpec, brushStampPoints, chooseCoordinateGuideStep, clampCanvasView, clampFloatingButton, coordinateGuideTicks, linePoints, normalizeBrushSize, restoreCanvasToolState, zoomCanvasView, } from "../canvas-tools-model.js"; import { setControlReady, setControlUnavailable } from "../interaction-feedback.js"; import { boardPoint, commitPixelChanges, fillPixelLayer, getBoardOverlay, getInteractionMode, getPixelBuffer, renderPixelDraft, } from "../scene-board.js"; const canvas = getBoardOverlay(); const localState = createLocalState("canvas-tools", 3); const previousLocalState = createLocalState("canvas-tools", 2).load(null) || createLocalState("canvas-tools", 1).load(null); const legacyState = createLocalState("canvas", 3).load(null) || createLocalState("canvas", 2).load(null) || {}; const toolButtons = Array.from(document.querySelectorAll("[data-canvas-tool]")); const paintColor = document.getElementById("paint-color"); const backgroundColor = document.getElementById("background-color"); const brushSizeInput = document.getElementById("brush-size"); const brushSizePresets = Array.from(document.querySelectorAll("[data-brush-size]")); const pixelEditToggle = document.getElementById("pixel-edit-toggle"); const pixelEditStatus = document.getElementById("pixel-edit-status"); const pixelGrid = document.getElementById("pixel-grid"); const boardStage = document.getElementById("board-stage"); const coordinateGuide = document.getElementById("canvas-coordinate-guide"); const coordinateGuideContext = coordinateGuide.getContext("2d"); const canvasWorkbench = document.querySelector(".canvas-workbench"); const startEditButton = document.getElementById("start-canvas-edit"); const assistive = document.getElementById("canvas-edit-assistive"); const assistiveToggle = document.getElementById("canvas-assistive-toggle"); const assistiveMenu = document.getElementById("canvas-assistive-menu"); const editorModeHint = document.getElementById("canvas-edit-mode-hint"); const coordinateGuideToggle = document.getElementById("toggle-canvas-coordinates"); const undoButton = document.getElementById("undo-canvas-edit"); const exitEditButton = document.getElementById("exit-canvas-edit"); const moveViewButton = document.getElementById("move-canvas-view"); const continueEditButton = document.getElementById("continue-canvas-edit"); const savedTools = restoreCanvasToolState(localState.load(null), previousLocalState || legacyState); let tool = savedTools.tool; let previousDrawingTool = tool; let pixelEditEnabled = savedTools.pixelEditEnabled; let normalBrushSize = savedTools.normalBrushSize; let pixelBrushSize = savedTools.pixelBrushSize; let coordinateGuideEnabled = savedTools.coordinateGuideEnabled; let drawing = false; let drawingPointerId = null; let lastPoint = null; let drawingBefore = null; let editorActive = false; let editorMode = "draw"; let view = { scale: 1, x: 0, y: 0 }; let viewGesture = null; let assistiveDrag = null; let suppressAssistiveClick = false; let coordinateGuideFrame = null; let assistivePosition = { x: 36, y: window.innerHeight / 2 }; const viewPointers = new Map(); const undoHistory = new PixelUndoHistory(); paintColor.value = savedTools.paintColor || paintColor.value; backgroundColor.value = savedTools.backgroundColor || backgroundColor.value; function brushMode() { return pixelEditEnabled ? BRUSH_MODE_PIXEL : BRUSH_MODE_NORMAL; } 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 activeRgb() { return tool === "eraser" ? hexToRgb(backgroundColor.value) : hexToRgb(paintColor.value); } function stampBrush(centerX, centerY, rgb) { brushStampPoints(centerX, centerY, currentBrushSize(), brushMode()) .forEach((point) => setPixel(point.x, point.y, rgb)); } function paintLine(from, to) { const rgb = activeRgb(); linePoints(from, to).forEach((point) => stampBrush(point.x, point.y, rgb)); } function paintEvent(event) { const samples = typeof event.getCoalescedEvents === "function" ? event.getCoalescedEvents() : [event]; for (const sample of samples.length ? samples : [event]) { const point = boardPoint(sample); paintLine(lastPoint || point, point); lastPoint = point; } renderPixelDraft(); } function pixelSnapshot() { return Uint8Array.from(getPixelBuffer()); } function updateUndoControl() { if (undoHistory.size > 0) setControlReady(undoButton); else setControlUnavailable(undoButton, "还没有可以撤销的像素操作。"); } function clearUndoHistory() { undoHistory.clear(); updateUndoControl(); } function recordPixelOperation(before) { const recorded = undoHistory.record(before, getPixelBuffer()); updateUndoControl(); return recorded; } function viewportBounds() { return { viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, baseSize: Math.max(1, boardStage.offsetWidth), minVisible: 48, }; } function hideCoordinateGuide() { if (coordinateGuideFrame !== null) { window.cancelAnimationFrame(coordinateGuideFrame); coordinateGuideFrame = null; } coordinateGuide.hidden = true; } function renderCoordinateGuide() { coordinateGuideFrame = null; if (!editorActive || !coordinateGuideEnabled || getInteractionMode() !== "pixels") { coordinateGuide.hidden = true; return; } const width = window.innerWidth; const height = window.innerHeight; const ratio = Math.max(1, window.devicePixelRatio || 1); const physicalWidth = Math.max(1, Math.round(width * ratio)); const physicalHeight = Math.max(1, Math.round(height * ratio)); if (coordinateGuide.width !== physicalWidth || coordinateGuide.height !== physicalHeight) { coordinateGuide.width = physicalWidth; coordinateGuide.height = physicalHeight; } coordinateGuide.hidden = false; coordinateGuideContext.setTransform(ratio, 0, 0, ratio, 0, 0); coordinateGuideContext.clearRect(0, 0, width, height); const rect = boardStage.getBoundingClientRect(); const left = Math.max(0, rect.left); const top = Math.max(0, rect.top); const right = Math.min(width, rect.right); const bottom = Math.min(height, rect.bottom); if (right <= left || bottom <= top || rect.width <= 0) return; const step = chooseCoordinateGuideStep(rect.width / 64); const xTicks = coordinateGuideTicks({ boardStart: rect.left, boardSize: rect.width, visibleStart: left, visibleEnd: right, step, }); const yTicks = coordinateGuideTicks({ boardStart: rect.top, boardSize: rect.height, visibleStart: top, visibleEnd: bottom, step, }); const rulerSize = 24; const rulerBottom = Math.min(bottom, top + rulerSize); const rulerRight = Math.min(right, left + rulerSize); coordinateGuideContext.save(); coordinateGuideContext.beginPath(); coordinateGuideContext.rect(left, top, right - left, bottom - top); coordinateGuideContext.clip(); coordinateGuideContext.beginPath(); for (const tick of xTicks) { coordinateGuideContext.moveTo(tick.position, top); coordinateGuideContext.lineTo(tick.position, bottom); } for (const tick of yTicks) { coordinateGuideContext.moveTo(left, tick.position); coordinateGuideContext.lineTo(right, tick.position); } coordinateGuideContext.strokeStyle = "rgba(255, 255, 255, 0.42)"; coordinateGuideContext.lineWidth = 1; coordinateGuideContext.stroke(); coordinateGuideContext.fillStyle = "rgba(255, 255, 255, 0.20)"; coordinateGuideContext.fillRect(left, top, right - left, rulerBottom - top); coordinateGuideContext.fillRect(left, top, rulerRight - left, bottom - top); coordinateGuideContext.fillStyle = "rgba(255, 255, 255, 0.34)"; coordinateGuideContext.fillRect(left, top, rulerRight - left, rulerBottom - top); coordinateGuideContext.beginPath(); for (const tick of xTicks) { coordinateGuideContext.moveTo(tick.position, rulerBottom - 7); coordinateGuideContext.lineTo(tick.position, rulerBottom); } for (const tick of yTicks) { coordinateGuideContext.moveTo(rulerRight - 7, tick.position); coordinateGuideContext.lineTo(rulerRight, tick.position); } coordinateGuideContext.strokeStyle = "rgba(255, 255, 255, 0.88)"; coordinateGuideContext.stroke(); coordinateGuideContext.fillStyle = "rgba(255, 255, 255, 0.96)"; coordinateGuideContext.font = "10px ui-monospace, SFMono-Regular, Consolas, monospace"; coordinateGuideContext.textBaseline = "top"; for (const tick of xTicks) { coordinateGuideContext.fillText(String(tick.value), tick.position + 2, top + 2); } for (const tick of yTicks) { coordinateGuideContext.fillText(String(tick.value), left + 2, tick.position + 2); } coordinateGuideContext.restore(); } function scheduleCoordinateGuideRender() { if (!editorActive || !coordinateGuideEnabled || getInteractionMode() !== "pixels") { hideCoordinateGuide(); return; } coordinateGuide.hidden = false; if (coordinateGuideFrame === null) { coordinateGuideFrame = window.requestAnimationFrame(renderCoordinateGuide); } } function updateCoordinateGuideControl() { coordinateGuideToggle.setAttribute("aria-pressed", String(coordinateGuideEnabled)); coordinateGuideToggle.textContent = coordinateGuideEnabled ? "隐藏坐标系" : "显示坐标系"; } function applyCanvasView(next = view) { view = clampCanvasView(next, viewportBounds()); boardStage.style.setProperty("--canvas-view-scale", String(view.scale)); boardStage.style.setProperty("--canvas-view-x", `${view.x}px`); boardStage.style.setProperty("--canvas-view-y", `${view.y}px`); scheduleCoordinateGuideRender(); } function resetCanvasView() { view = { scale: 1, x: 0, y: 0 }; applyCanvasView(); } function applyAssistivePosition(next = assistivePosition) { assistivePosition = clampFloatingButton(next, { viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, size: 56, inset: 8, }); assistive.style.setProperty("--assistive-x", `${assistivePosition.x}px`); assistive.style.setProperty("--assistive-y", `${assistivePosition.y}px`); assistive.dataset.menuHorizontal = assistivePosition.x > window.innerWidth / 2 ? "left" : "right"; assistive.dataset.menuVertical = assistivePosition.y > window.innerHeight / 2 ? "up" : "down"; } function resetAssistivePosition() { applyAssistivePosition({ x: 36, y: window.innerHeight / 2 }); } function closeAssistiveMenu({ restoreFocus = false } = {}) { assistiveMenu.hidden = true; assistiveToggle.setAttribute("aria-expanded", "false"); assistiveToggle.setAttribute("aria-label", "打开画布编辑菜单"); if (restoreFocus && editorActive) assistiveToggle.focus(); } function openAssistiveMenu() { applyAssistivePosition(); assistiveMenu.hidden = false; assistiveToggle.setAttribute("aria-expanded", "true"); assistiveToggle.setAttribute("aria-label", "关闭画布编辑菜单"); const firstAvailable = assistiveMenu.querySelector('button:not([aria-disabled="true"])'); firstAvailable?.focus(); } function toggleAssistiveMenu() { if (assistiveMenu.hidden) openAssistiveMenu(); else closeAssistiveMenu({ restoreFocus: true }); } function updateEditorModeControls() { const moving = editorMode === "move"; if (editorActive) document.body.dataset.canvasEditorMode = editorMode; else delete document.body.dataset.canvasEditorMode; editorModeHint.textContent = moving ? "移动模式:单指拖动画布,双指捏合缩放;桌面可使用滚轮缩放。" : "绘画模式:直接在画布上绘制。"; if (moving) { setControlUnavailable(moveViewButton, "当前已经是移动画布视线模式。"); setControlReady(continueEditButton); } else { setControlReady(moveViewButton); setControlUnavailable(continueEditButton, "当前已经是绘画模式。"); } updateUndoControl(); } function resetViewGesture() { const points = Array.from(viewPointers.values()); if (points.length >= 2) { const [first, second] = points; viewGesture = { kind: "pinch", startView: { ...view }, startMidpoint: { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 }, startDistance: Math.max(1, Math.hypot(second.x - first.x, second.y - first.y)), }; } else if (points.length === 1) { viewGesture = { kind: "pan", startView: { ...view }, startPoint: { ...points[0] } }; } else { viewGesture = null; } } function setEditorMode(mode) { if (!editorActive) return; if (mode === "move") finishDrawing(); editorMode = mode === "move" ? "move" : "draw"; viewPointers.clear(); viewGesture = null; updateEditorModeControls(); closeAssistiveMenu({ restoreFocus: true }); announce(editorMode === "move" ? "已进入移动画布视线模式:单指拖动,双指缩放" : "已继续编辑画布"); } function enterCanvasEditor() { if (editorActive || getInteractionMode() !== "pixels") return; editorActive = true; editorMode = "draw"; canvas.dataset.canvasEditing = "true"; document.body.classList.add("canvas-edit-active"); assistive.hidden = false; closeAssistiveMenu(); updateEditorModeControls(); updateCoordinateGuideControl(); window.requestAnimationFrame(() => { resetCanvasView(); resetAssistivePosition(); scheduleCoordinateGuideRender(); assistiveToggle.focus(); }); announce("已进入全屏画布编辑;左侧悬浮按钮可撤销、移动视线或退出"); } function exitCanvasEditor({ returnToTools = true } = {}) { if (!editorActive) return; finishDrawing(); editorActive = false; editorMode = "draw"; viewPointers.clear(); viewGesture = null; closeAssistiveMenu(); assistive.hidden = true; hideCoordinateGuide(); delete canvas.dataset.canvasEditing; document.body.classList.remove("canvas-edit-active"); delete document.body.dataset.canvasEditorMode; boardStage.style.removeProperty("--canvas-view-scale"); boardStage.style.removeProperty("--canvas-view-x"); boardStage.style.removeProperty("--canvas-view-y"); view = { scale: 1, x: 0, y: 0 }; if (returnToTools) { window.requestAnimationFrame(() => { canvasWorkbench.scrollIntoView({ block: "start", behavior: "smooth" }); startEditButton.focus({ preventScroll: true }); }); } announce("已退出画布编辑,可以重新选择颜色和画笔工具"); } function undoPixelOperation() { const previous = undoHistory.undo(); if (!previous) { updateUndoControl(); return; } getPixelBuffer().set(previous); commitPixelChanges("pixel-undo"); updateUndoControl(); announce("已撤销上一个像素操作"); } function persistTools() { localState.save({ paintColor: paintColor.value, backgroundColor: backgroundColor.value, tool, pixelEditEnabled, normalBrushSize, pixelBrushSize, coordinateGuideEnabled, }); } function toggleCoordinateGuide() { coordinateGuideEnabled = !coordinateGuideEnabled; updateCoordinateGuideControl(); persistTools(); scheduleCoordinateGuideRender(); closeAssistiveMenu({ restoreFocus: true }); announce(coordinateGuideEnabled ? "已显示画布坐标系,标尺和辅助线会随视角缩放" : "已隐藏画布坐标系"); } function renderBrushSize() { const mode = brushMode(); const brushSize = currentBrushSize(); const spec = brushModeSpec(mode); brushSizeInput.min = String(spec.min); brushSizeInput.max = String(spec.max); brushSizeInput.value = String(brushSize); document.getElementById("brush-size-value").textContent = pixelEditEnabled ? `${brushSize} × ${brushSize}` : `${brushSize} px`; const preview = document.getElementById("brush-size-preview"); const previewSize = Math.max(4, brushSize * 2); preview.style.width = `${previewSize}px`; preview.style.height = `${previewSize}px`; preview.dataset.shape = pixelEditEnabled ? "square" : "round"; brushSizePresets.forEach((button, index) => { const value = spec.presets[index]; button.dataset.brushSize = String(value); button.textContent = String(value); }); setActiveButton( brushSizePresets, brushSizePresets.find((button) => Number(button.dataset.brushSize) === brushSize), ); pixelEditToggle.setAttribute("aria-pressed", String(pixelEditEnabled)); pixelEditToggle.textContent = pixelEditEnabled ? "退出单像素编辑" : "开启单像素编辑"; pixelEditStatus.hidden = !pixelEditEnabled; pixelEditStatus.textContent = pixelEditEnabled ? `画布已按 64×64 方形像素分格,当前笔尖为 ${brushSize}×${brushSize}。` : ""; } function setBrushSize(value) { const normalized = normalizeBrushSize(value, brushMode()); if (pixelEditEnabled) pixelBrushSize = normalized; else normalBrushSize = normalized; renderBrushSize(); persistTools(); } function updatePixelGridVisibility() { pixelGrid.hidden = !(pixelEditEnabled && getInteractionMode() === "pixels"); } function togglePixelEditMode() { pixelEditEnabled = !pixelEditEnabled; renderBrushSize(); updatePixelGridVisibility(); persistTools(); announce(pixelEditEnabled ? `已进入单像素编辑模式:画布已按 64×64 方形像素分格,笔尖为 ${pixelBrushSize}×${pixelBrushSize} 正方形。` : `已退出单像素编辑模式:恢复 ${normalBrushSize} px 圆形笔尖。`); } function selectTool(button) { tool = button.dataset.canvasTool; if (tool !== "eyedropper") previousDrawingTool = tool; setActiveButton(toolButtons, button); persistTools(); } toolButtons.forEach((button) => button.addEventListener("click", () => selectTool(button))); canvas.addEventListener("pointerdown", (event) => { if (!editorActive || getInteractionMode() !== "pixels") return; if (editorMode === "move") { event.preventDefault(); viewPointers.set(event.pointerId, { x: event.clientX, y: event.clientY }); canvas.setPointerCapture(event.pointerId); resetViewGesture(); return; } if (drawing) return; if (tool === "eyedropper") { const point = boardPoint(event); const offset = (point.y * 64 + point.x) * 3; const pixels = getPixelBuffer(); setColorValue(paintColor, rgbToHex({ r: pixels[offset], g: pixels[offset + 1], b: pixels[offset + 2], }), { recordHistory: true }); tool = previousDrawingTool; setActiveButton(toolButtons, toolButtons.find((button) => button.dataset.canvasTool === tool)); persistTools(); announce(`已吸取像素层 (${point.x}, ${point.y}) 的颜色`); return; } event.preventDefault(); drawing = true; drawingPointerId = event.pointerId; drawingBefore = pixelSnapshot(); lastPoint = boardPoint(event); canvas.setPointerCapture(event.pointerId); paintEvent(event); }); canvas.addEventListener("pointermove", (event) => { if (editorActive && editorMode === "move" && viewPointers.has(event.pointerId)) { event.preventDefault(); viewPointers.set(event.pointerId, { x: event.clientX, y: event.clientY }); const points = Array.from(viewPointers.values()); if (viewGesture?.kind === "pinch" && points.length >= 2) { const [first, second] = points; const midpoint = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 }; const distance = Math.max(1, Math.hypot(second.x - first.x, second.y - first.y)); const scaled = zoomCanvasView( viewGesture.startView, viewGesture.startView.scale * (distance / viewGesture.startDistance), viewGesture.startMidpoint, viewportBounds(), ); applyCanvasView({ ...scaled, x: scaled.x + midpoint.x - viewGesture.startMidpoint.x, y: scaled.y + midpoint.y - viewGesture.startMidpoint.y, }); } else if (viewGesture?.kind === "pan" && points.length === 1) { applyCanvasView({ ...viewGesture.startView, x: viewGesture.startView.x + points[0].x - viewGesture.startPoint.x, y: viewGesture.startView.y + points[0].y - viewGesture.startPoint.y, }); } return; } if (drawing && drawingPointerId === event.pointerId && editorActive && editorMode === "draw" && getInteractionMode() === "pixels") paintEvent(event); }); function finishDrawing() { if (!drawing) return; drawing = false; drawingPointerId = null; lastPoint = null; const changed = recordPixelOperation(drawingBefore); drawingBefore = null; if (changed) commitPixelChanges("pixel-draw"); persistTools(); } function finishCanvasPointer(event) { if (viewPointers.delete(event.pointerId)) { resetViewGesture(); return; } if (drawingPointerId === event.pointerId) finishDrawing(); } canvas.addEventListener("pointerup", finishCanvasPointer); canvas.addEventListener("pointercancel", finishCanvasPointer); canvas.addEventListener("lostpointercapture", finishCanvasPointer); canvas.addEventListener("wheel", (event) => { if (!editorActive || editorMode !== "move") return; event.preventDefault(); const factor = Math.exp(-event.deltaY * 0.002); applyCanvasView(zoomCanvasView(view, view.scale * factor, { x: event.clientX, y: event.clientY, }, viewportBounds())); }, { passive: false }); paintColor.addEventListener("input", persistTools); backgroundColor.addEventListener("input", persistTools); brushSizeInput.addEventListener("input", () => setBrushSize(brushSizeInput.value)); brushSizePresets.forEach((button) => button.addEventListener("click", () => setBrushSize(button.dataset.brushSize))); pixelEditToggle.addEventListener("click", togglePixelEditMode); coordinateGuideToggle.addEventListener("click", toggleCoordinateGuide); document.getElementById("fill-canvas").addEventListener("click", () => { const before = pixelSnapshot(); const rgb = hexToRgb(backgroundColor.value); fillPixelLayer([rgb.r, rgb.g, rgb.b]); recordPixelOperation(before); announce("背景颜色已填充到像素层,文字元素保持不变"); }); document.getElementById("clear-canvas").addEventListener("click", () => { const before = pixelSnapshot(); fillPixelLayer([0, 0, 0]); recordPixelOperation(before); announce("像素层已清空,文字元素保持不变"); }); startEditButton.addEventListener("click", enterCanvasEditor); undoButton.addEventListener("click", undoPixelOperation); exitEditButton.addEventListener("click", () => exitCanvasEditor()); moveViewButton.addEventListener("click", () => setEditorMode("move")); continueEditButton.addEventListener("click", () => setEditorMode("draw")); assistiveToggle.addEventListener("pointerdown", (event) => { if (!editorActive) return; event.preventDefault(); event.stopPropagation(); assistiveDrag = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, origin: { ...assistivePosition }, moved: false, }; assistiveToggle.setPointerCapture(event.pointerId); }); assistiveToggle.addEventListener("pointermove", (event) => { if (!assistiveDrag || assistiveDrag.pointerId !== event.pointerId) return; event.preventDefault(); event.stopPropagation(); const dx = event.clientX - assistiveDrag.startX; const dy = event.clientY - assistiveDrag.startY; if (Math.hypot(dx, dy) > 6) assistiveDrag.moved = true; if (assistiveDrag.moved) { closeAssistiveMenu(); applyAssistivePosition({ x: assistiveDrag.origin.x + dx, y: assistiveDrag.origin.y + dy }); } }); function finishAssistiveDrag(event) { if (!assistiveDrag || assistiveDrag.pointerId !== event.pointerId) return; suppressAssistiveClick = assistiveDrag.moved; assistiveDrag = null; event.stopPropagation(); } assistiveToggle.addEventListener("pointerup", finishAssistiveDrag); assistiveToggle.addEventListener("pointercancel", finishAssistiveDrag); assistiveToggle.addEventListener("lostpointercapture", finishAssistiveDrag); assistiveToggle.addEventListener("click", () => { if (suppressAssistiveClick) { suppressAssistiveClick = false; return; } toggleAssistiveMenu(); }); document.addEventListener("keydown", (event) => { if (!editorActive || event.key !== "Escape") return; event.preventDefault(); if (!assistiveMenu.hidden) closeAssistiveMenu({ restoreFocus: true }); else exitCanvasEditor(); }); window.addEventListener("resize", () => { if (!editorActive) return; applyCanvasView(); applyAssistivePosition(); }); document.addEventListener("matrix:composition-applied", clearUndoHistory); document.addEventListener("matrix:canvas-history-checkpoint", clearUndoHistory); document.addEventListener("matrix:scene-replaced", clearUndoHistory); refreshColorTrigger(paintColor); refreshColorTrigger(backgroundColor); setActiveButton(toolButtons, toolButtons.find((button) => button.dataset.canvasTool === tool)); renderBrushSize(); updateCoordinateGuideControl(); persistTools(); updateEditorModeControls(); registerWorkspace({ id: "canvas", title: "像素画布", elementId: "workspace-canvas", order: 30, persistence: "local", capabilities: ["scene-template-save"], onEnter() { renderPixelDraft(); updatePixelGridVisibility(); }, onLeave() { exitCanvasEditor({ returnToTools: false }); finishDrawing(); pixelGrid.hidden = true; hideCoordinateGuide(); }, });