Add adaptive coordinate guides to canvas editor
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
| `WEB-DIAGNOSTIC` | 硬件诊断图案 | 支持发送四角横线、行带、地址检查和文字复现图案,用于排查重复/错位显示。 |
|
| `WEB-DIAGNOSTIC` | 硬件诊断图案 | 支持发送四角横线、行带、地址检查和文字复现图案,用于排查重复/错位显示。 |
|
||||||
| `WEB-CANVAS` | 像素底层和 WebSocket 帧 | 维护 `64 x 64 x RGB` 像素底层,合成后通过 `WS /ws/canvas` 发送最终整帧。 |
|
| `WEB-CANVAS` | 像素底层和 WebSocket 帧 | 维护 `64 x 64 x RGB` 像素底层,合成后通过 `WS /ws/canvas` 发送最终整帧。 |
|
||||||
| `WEB-CANVAS-EDIT` | 防误触全屏编辑与画布视角 | 像素画布默认只读,经明确入口进入全屏绘画;编辑期提供可移动悬浮菜单、会话撤销以及独立的平移缩放模式。 |
|
| `WEB-CANVAS-EDIT` | 防误触全屏编辑与画布视角 | 像素画布默认只读,经明确入口进入全屏绘画;编辑期提供可移动悬浮菜单、会话撤销以及独立的平移缩放模式。 |
|
||||||
|
| `WEB-CANVAS-COORDINATES` | 自适应画布坐标系 | 全屏像素编辑可从悬浮菜单开关浏览器本地记忆的坐标标尺与贯穿辅助线,随平移和缩放自适应且不进入任何场景或输出。 |
|
||||||
| `WEB-COLOR-UI` | 统一颜色选择与绘画工具 | 使用软件自有的 RGB/HSV 取色面板、最近颜色、共享色板和触摸友好的画笔工具,不调用系统颜色选择器。 |
|
| `WEB-COLOR-UI` | 统一颜色选择与绘画工具 | 使用软件自有的 RGB/HSV 取色面板、最近颜色、共享色板和触摸友好的画笔工具,不调用系统颜色选择器。 |
|
||||||
| `WEB-ORIENTATION` | 页面方向设置 | 支持 `0/90/180/270`,保存后重启仍生效。 |
|
| `WEB-ORIENTATION` | 页面方向设置 | 支持 `0/90/180/270`,保存后重启仍生效。 |
|
||||||
| `WEB-BRIGHTNESS` | 页面实时亮度设置 | 拖动亮度条时持续应用并保存最新值,当前画面立即反馈且不需要再次发送。 |
|
| `WEB-BRIGHTNESS` | 页面实时亮度设置 | 拖动亮度条时持续应用并保存最新值,当前画面立即反馈且不需要再次发送。 |
|
||||||
@@ -451,6 +452,10 @@ WantedBy=multi-user.target
|
|||||||
- 共享画板在像素工作区默认只读,触摸不会落笔或阻止页面滚动;颜色和画笔工具区提供明确的“开始编辑”按钮,点击后使用现有共享画板进入 CSS 全视口专注编辑,不调用浏览器 Fullscreen API。
|
- 共享画板在像素工作区默认只读,触摸不会落笔或阻止页面滚动;颜色和画笔工具区提供明确的“开始编辑”按钮,点击后使用现有共享画板进入 CSS 全视口专注编辑,不调用浏览器 Fullscreen API。
|
||||||
- 专注编辑默认进入绘画模式并锁定页面滚动;退出时完成已改变的当前笔画、复位视角并回到颜色和画笔工具区。离开像素工作区也必须先安全退出。文字工作区的画板选择、拖动和缩放行为不受该状态影响。
|
- 专注编辑默认进入绘画模式并锁定页面滚动;退出时完成已改变的当前笔画、复位视角并回到颜色和画笔工具区。离开像素工作区也必须先安全退出。文字工作区的画板选择、拖动和缩放行为不受该状态影响。
|
||||||
- 专注编辑内持续显示类似 AssistiveTouch 的圆形悬浮按钮。按钮初始位于左侧中部,可在安全区内拖动且不得产生画笔输入;点击展开当前模式提示、撤销、退出编辑、移动画布视线和继续编辑,菜单按剩余空间调整展开方向并维护键盘焦点、`aria-expanded` 和不可用原因。
|
- 专注编辑内持续显示类似 AssistiveTouch 的圆形悬浮按钮。按钮初始位于左侧中部,可在安全区内拖动且不得产生画笔输入;点击展开当前模式提示、撤销、退出编辑、移动画布视线和继续编辑,菜单按剩余空间调整展开方向并维护键盘焦点、`aria-expanded` 和不可用原因。
|
||||||
|
- 悬浮菜单提供独立的“显示坐标系/隐藏坐标系”同步开关并使用 `aria-pressed`。坐标系默认关闭,选择按浏览器本地工具状态记忆;它只在像素画布的全屏专注编辑中显示,退出编辑或离开工作区时隐藏但不清除选择。
|
||||||
|
- 坐标系是 `pointer-events:none` 的高 DPI 网页辅助层。在画板可见区域内侧的顶部和左侧显示约 `24 CSS px` 的半透明标尺、`0..63` 坐标数字和刻度,主刻度以低透明度细线贯穿画板;坐标始终以左上角为 `(0,0)`,不得改变绘画命中区域或阻止任何画布手势。
|
||||||
|
- 坐标主刻度根据变换后单个逻辑像素的 CSS 尺寸,从 `1/2/4/8/16/32` 逻辑像素间隔中选取最接近约 `48 CSS px` 的档位。平移、捏合、滚轮缩放、视口或设备像素比变化后必须重新对齐;单像素模式的精确 `64x64` 网格保持原样并可与坐标主线同时显示。
|
||||||
|
- 坐标系不得写入场景、模板、动图、设备配置或显示帧,也不得出现在导出 PNG 和 WebSocket RGB888 数据中。开关只写入当前浏览器的带版本工具状态,从旧版本迁移时默认关闭。
|
||||||
- 移动画布视线时禁止落笔,单指平移、双指以中点为锚在 `1x..8x` 内缩放,桌面滚轮按指针位置缩放;切回继续编辑后保留视角并按变换后的画板矩形换算逻辑坐标。视口变化后重新约束位置,画布不得完全移出可见区。
|
- 移动画布视线时禁止落笔,单指平移、双指以中点为锚在 `1x..8x` 内缩放,桌面滚轮按指针位置缩放;切回继续编辑后保留视角并按变换后的画板矩形换算逻辑坐标。视口变化后重新约束位置,画布不得完全移出可见区。
|
||||||
- 撤销历史按每一笔画笔/橡皮、填充背景和清空像素层分组,只在像素确实变化时记录操作前的 RGB 快照;最多 50 步,只存在当前页面内存。撤销仅恢复像素层并保留文字。退出再进入专注编辑不清空历史;页面刷新、场景替换,以及成功应用统一画板、保存或更新模板、保存或新建动图帧后清空,失败提交不得清空。
|
- 撤销历史按每一笔画笔/橡皮、填充背景和清空像素层分组,只在像素确实变化时记录操作前的 RGB 快照;最多 50 步,只存在当前页面内存。撤销仅恢复像素层并保留文字。退出再进入专注编辑不清空历史;页面刷新、场景替换,以及成功应用统一画板、保存或更新模板、保存或新建动图帧后清空,失败提交不得清空。
|
||||||
- 支持“应用统一画板”和“保存 PNG”;PNG 保存导出最终组合场景,不只导出像素底层。
|
- 支持“应用统一画板”和“保存 PNG”;PNG 保存导出最终组合场景,不只导出像素底层。
|
||||||
@@ -501,6 +506,7 @@ WebSocket 消息示例:
|
|||||||
- 画布快速拖动时服务不崩溃,最多丢弃旧帧。
|
- 画布快速拖动时服务不崩溃,最多丢弃旧帧。
|
||||||
- 单像素模式的 `1x1/2x2/3x3/4x4` 笔尖均为完整正方形,快速拖动不留采样断线,边缘按 `0..63` 精确裁剪;网格在深浅像素上可辨认且不改变导出或发送的 RGB。
|
- 单像素模式的 `1x1/2x2/3x3/4x4` 笔尖均为完整正方形,快速拖动不留采样断线,边缘按 `0..63` 精确裁剪;网格在深浅像素上可辨认且不改变导出或发送的 RGB。
|
||||||
- 约 `320px` 和常见手机视口下,未进入编辑时在画板上滑动页面不会改变像素;进入专注编辑后悬浮按钮拖动不落笔,移动模式可平移缩放,继续编辑后的落点仍与视觉像素一致。
|
- 约 `320px` 和常见手机视口下,未进入编辑时在画板上滑动页面不会改变像素;进入专注编辑后悬浮按钮拖动不落笔,移动模式可平移缩放,继续编辑后的落点仍与视觉像素一致。
|
||||||
|
- 坐标系在约 `320px`、`390px`、平板和桌面视口以及 `1x/2x/4x/8x` 视角下保持刻度、数字和贯穿线对齐,缩放时自动调整密度;普通和单像素绘画、吸管、撤销、平移、缩放及最终 PNG/RGB888 在开关前后保持一致。
|
||||||
- 撤销可连续恢复所有未提交像素操作;成功提交当前场景后撤销立即不可用,失败提交仍可撤销。历史不写入浏览器持久存储、设备配置、模板或动图记录。
|
- 撤销可连续恢复所有未提交像素操作;成功提交当前场景后撤销立即不可用,失败提交仍可撤销。历史不写入浏览器持久存储、设备配置、模板或动图记录。
|
||||||
|
|
||||||
### 2.3.1 统一颜色选择器(`WEB-COLOR-UI`、`CONFIG-COLOR-PALETTE`)
|
### 2.3.1 统一颜色选择器(`WEB-COLOR-UI`、`CONFIG-COLOR-PALETTE`)
|
||||||
@@ -1143,7 +1149,7 @@ WebSocket 消息示例:
|
|||||||
- 只有新内容已完整校验并成功写入稳定键后,才精确删除本次已迁移的旧键;写入失败时旧键和值必须保留。
|
- 只有新内容已完整校验并成功写入稳定键后,才精确删除本次已迁移的旧键;写入失败时旧键和值必须保留。
|
||||||
- 稳定键内容损坏、出现未知更高版本或迁移失败时,页面保留原值和内存草稿、停止自动写回并明确提示用户;不得用空白场景覆盖,不得自动上传到核桃派。场景只保存在当前浏览器,不写入设备 `config.json`,不要求跨浏览器共享,也不保证核桃派重启后自动恢复实屏。
|
- 稳定键内容损坏、出现未知更高版本或迁移失败时,页面保留原值和内存草稿、停止自动写回并明确提示用户;不得用空白场景覆盖,不得自动上传到核桃派。场景只保存在当前浏览器,不写入设备 `config.json`,不要求跨浏览器共享,也不保证核桃派重启后自动恢复实屏。
|
||||||
- 同一来源的其他标签页修改稳定场景键时不得直接替换当前内存草稿;冲突解决前当前标签页的后续编辑继续保留在内存但暂停写回 `localStorage`。用户选择“载入其他标签草稿”后采用最后收到的有效外部值,选择“保留当前草稿”后才显式覆盖共享键。
|
- 同一来源的其他标签页修改稳定场景键时不得直接替换当前内存草稿;冲突解决前当前标签页的后续编辑继续保留在内存但暂停写回 `localStorage`。用户选择“载入其他标签草稿”后采用最后收到的有效外部值,选择“保留当前草稿”后才显式覆盖共享键。
|
||||||
- 画笔颜色、像素层背景颜色、画笔模式、两种模式各自的笔尖粗细和当前画笔工具使用独立带版本号的本地 UI 状态;它们不是场景元素,也不改变 scene v1 的最终像素和元素契约。当前键为 `matrixController:canvas-tools:v2`:刷新后恢复模式与两套尺寸;首次从 v1 读取时把旧 `brushSize` 迁入普通模式,像素模式默认 `1x1` 且默认关闭,损坏或缺失值安全回落到默认值。
|
- 画笔颜色、像素层背景颜色、画笔模式、两种模式各自的笔尖粗细、当前画笔工具和坐标系开关使用独立带版本号的本地 UI 状态;它们不是场景元素,也不改变 scene v1 的最终像素和元素契约。当前键为 `matrixController:canvas-tools:v3`:刷新后恢复模式、两套尺寸和坐标系选择;首次从 v2/v1 读取时保留已有工具值、把旧 `brushSize` 迁入普通模式,像素模式默认 `1x1` 且默认关闭,坐标系默认关闭,损坏或缺失值安全回落到默认值。
|
||||||
- 每个新工作区显式选择 `none`、`local` 或 `server`;普通显示功能必须说明怎样接入 scene v1,不得把未来场景大对象无条件塞入服务端配置。
|
- 每个新工作区显式选择 `none`、`local` 或 `server`;普通显示功能必须说明怎样接入 scene v1,不得把未来场景大对象无条件塞入服务端配置。
|
||||||
|
|
||||||
核桃派模板(`CONFIG-TEMPLATES`)使用持久根下独立的 `templates/` 数据区:
|
核桃派模板(`CONFIG-TEMPLATES`)使用持久根下独立的 `templates/` 数据区:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2026-09-08T10:19+08:00
|
2026-09-10T11:10+08:00
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export const BRUSH_MODE_PIXEL = "pixel";
|
|||||||
export const CANVAS_VIEW_MIN_SCALE = 1;
|
export const CANVAS_VIEW_MIN_SCALE = 1;
|
||||||
export const CANVAS_VIEW_MAX_SCALE = 8;
|
export const CANVAS_VIEW_MAX_SCALE = 8;
|
||||||
export const CANVAS_UNDO_LIMIT = 50;
|
export const CANVAS_UNDO_LIMIT = 50;
|
||||||
|
export const COORDINATE_GUIDE_STEPS = Object.freeze([1, 2, 4, 8, 16, 32]);
|
||||||
|
export const COORDINATE_GUIDE_TARGET_SPACING = 48;
|
||||||
|
|
||||||
const MODE_SPECS = Object.freeze({
|
const MODE_SPECS = Object.freeze({
|
||||||
[BRUSH_MODE_NORMAL]: Object.freeze({ min: 1, max: 16, presets: Object.freeze([1, 2, 4, 8]) }),
|
[BRUSH_MODE_NORMAL]: Object.freeze({ min: 1, max: 16, presets: Object.freeze([1, 2, 4, 8]) }),
|
||||||
@@ -23,23 +25,55 @@ export function normalizeBrushSize(value, mode) {
|
|||||||
return Math.max(spec.min, Math.min(spec.max, Number.isFinite(parsed) ? parsed : spec.min));
|
return Math.max(spec.min, Math.min(spec.max, Number.isFinite(parsed) ? parsed : spec.min));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restoreCanvasToolState(savedV2, savedV1 = null) {
|
export function restoreCanvasToolState(savedV3, savedV2 = null) {
|
||||||
const current = record(savedV2);
|
const current = record(savedV3);
|
||||||
const legacy = record(savedV1);
|
const legacy = record(savedV2);
|
||||||
const source = current || legacy || {};
|
const source = current || legacy || {};
|
||||||
return {
|
return {
|
||||||
paintColor: typeof source.paintColor === "string" ? source.paintColor : null,
|
paintColor: typeof source.paintColor === "string" ? source.paintColor : null,
|
||||||
backgroundColor: typeof source.backgroundColor === "string" ? source.backgroundColor : null,
|
backgroundColor: typeof source.backgroundColor === "string" ? source.backgroundColor : null,
|
||||||
tool: source.tool === "eraser" ? "eraser" : "brush",
|
tool: source.tool === "eraser" ? "eraser" : "brush",
|
||||||
pixelEditEnabled: Boolean(current?.pixelEditEnabled),
|
pixelEditEnabled: Boolean(source.pixelEditEnabled),
|
||||||
normalBrushSize: normalizeBrushSize(
|
normalBrushSize: normalizeBrushSize(
|
||||||
current?.normalBrushSize ?? source.brushSize,
|
source.normalBrushSize ?? source.brushSize,
|
||||||
BRUSH_MODE_NORMAL,
|
BRUSH_MODE_NORMAL,
|
||||||
),
|
),
|
||||||
pixelBrushSize: normalizeBrushSize(current?.pixelBrushSize, BRUSH_MODE_PIXEL),
|
pixelBrushSize: normalizeBrushSize(source.pixelBrushSize, BRUSH_MODE_PIXEL),
|
||||||
|
coordinateGuideEnabled: Boolean(current?.coordinateGuideEnabled),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function chooseCoordinateGuideStep(
|
||||||
|
logicalPixelCssSize,
|
||||||
|
targetSpacing = COORDINATE_GUIDE_TARGET_SPACING,
|
||||||
|
) {
|
||||||
|
const pixelSize = Math.max(0, Number(logicalPixelCssSize) || 0);
|
||||||
|
const target = Math.max(1, Number(targetSpacing) || COORDINATE_GUIDE_TARGET_SPACING);
|
||||||
|
return COORDINATE_GUIDE_STEPS.reduce((best, candidate) => (
|
||||||
|
Math.abs(candidate * pixelSize - target) < Math.abs(best * pixelSize - target)
|
||||||
|
? candidate
|
||||||
|
: best
|
||||||
|
), COORDINATE_GUIDE_STEPS[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coordinateGuideTicks({
|
||||||
|
boardStart,
|
||||||
|
boardSize,
|
||||||
|
visibleStart = Number.NEGATIVE_INFINITY,
|
||||||
|
visibleEnd = Number.POSITIVE_INFINITY,
|
||||||
|
step,
|
||||||
|
}) {
|
||||||
|
const start = Number(boardStart) || 0;
|
||||||
|
const size = Math.max(0, Number(boardSize) || 0);
|
||||||
|
const interval = COORDINATE_GUIDE_STEPS.includes(step) ? step : COORDINATE_GUIDE_STEPS[0];
|
||||||
|
const ticks = [];
|
||||||
|
for (let value = 0; value < 64; value += interval) {
|
||||||
|
const position = start + (value / 64) * size;
|
||||||
|
if (position >= visibleStart && position <= visibleEnd) ticks.push({ value, position });
|
||||||
|
}
|
||||||
|
return ticks;
|
||||||
|
}
|
||||||
|
|
||||||
export function linePoints(from, to) {
|
export function linePoints(from, to) {
|
||||||
const points = [];
|
const points = [];
|
||||||
let x = from.x;
|
let x = from.x;
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
<canvas id="scene-overlay" width="64" height="64" aria-label="统一画板交互层"></canvas>
|
<canvas id="scene-overlay" width="64" height="64" aria-label="统一画板交互层"></canvas>
|
||||||
<div id="pixel-grid" class="pixel-grid" aria-hidden="true" hidden></div>
|
<div id="pixel-grid" class="pixel-grid" aria-hidden="true" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
|
<canvas id="canvas-coordinate-guide" class="canvas-coordinate-guide" aria-hidden="true" hidden></canvas>
|
||||||
<p id="board-status" class="board-status" aria-live="polite">正在恢复画板草稿…</p>
|
<p id="board-status" class="board-status" aria-live="polite">正在恢复画板草稿…</p>
|
||||||
<div class="board-actions">
|
<div class="board-actions">
|
||||||
<button id="apply-composition" class="primary-button" type="button">应用统一画板</button>
|
<button id="apply-composition" class="primary-button" type="button">应用统一画板</button>
|
||||||
@@ -118,6 +119,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div id="canvas-assistive-menu" class="canvas-assistive-menu" role="menu" aria-label="画布编辑操作" hidden>
|
<div id="canvas-assistive-menu" class="canvas-assistive-menu" role="menu" aria-label="画布编辑操作" hidden>
|
||||||
<p id="canvas-edit-mode-hint" role="status" aria-live="polite">绘画模式:直接在画布上绘制</p>
|
<p id="canvas-edit-mode-hint" role="status" aria-live="polite">绘画模式:直接在画布上绘制</p>
|
||||||
|
<button id="toggle-canvas-coordinates" type="button" role="menuitem" aria-pressed="false">显示坐标系</button>
|
||||||
<button id="undo-canvas-edit" type="button" role="menuitem" aria-disabled="true"
|
<button id="undo-canvas-edit" type="button" role="menuitem" aria-disabled="true"
|
||||||
data-unavailable-reason="还没有可以撤销的像素操作。">撤销</button>
|
data-unavailable-reason="还没有可以撤销的像素操作。">撤销</button>
|
||||||
<button id="exit-canvas-edit" type="button" role="menuitem">退出编辑</button>
|
<button id="exit-canvas-edit" type="button" role="menuitem">退出编辑</button>
|
||||||
|
|||||||
@@ -905,6 +905,15 @@ h3 {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.canvas-coordinate-guide {
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-coordinate-guide[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
#scene-overlay[data-interaction="readonly"] {
|
#scene-overlay[data-interaction="readonly"] {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -950,6 +959,17 @@ body.canvas-edit-active .board-stage {
|
|||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.canvas-edit-active .canvas-coordinate-guide:not([hidden]) {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1005;
|
||||||
|
inset: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
mix-blend-mode: difference;
|
||||||
|
}
|
||||||
|
|
||||||
body.canvas-edit-active[data-canvas-editor-mode="move"] .board-stage {
|
body.canvas-edit-active[data-canvas-editor-mode="move"] .board-stage {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3317,6 +3317,18 @@
|
|||||||
"source": "index.html::#workspace-device > article:nth-of-type(3) > div:nth-of-type(1) > h3:nth-of-type(1)::text[0]",
|
"source": "index.html::#workspace-device > article:nth-of-type(3) > div:nth-of-type(1) > h3:nth-of-type(1)::text[0]",
|
||||||
"text": "设备容量"
|
"text": "设备容量"
|
||||||
},
|
},
|
||||||
|
"copy.83ad7143ff410a09": {
|
||||||
|
"kind": "html_text",
|
||||||
|
"placeholders": [],
|
||||||
|
"render": {
|
||||||
|
"selector": "#toggle-canvas-coordinates",
|
||||||
|
"text_index": 0,
|
||||||
|
"type": "static_text"
|
||||||
|
},
|
||||||
|
"scope": "global",
|
||||||
|
"source": "index.html::#toggle-canvas-coordinates::text[0]",
|
||||||
|
"text": "显示坐标系"
|
||||||
|
},
|
||||||
"copy.83e4a810c846111b": {
|
"copy.83e4a810c846111b": {
|
||||||
"kind": "html_text",
|
"kind": "html_text",
|
||||||
"placeholders": [],
|
"placeholders": [],
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import {
|
|||||||
PixelUndoHistory,
|
PixelUndoHistory,
|
||||||
brushModeSpec,
|
brushModeSpec,
|
||||||
brushStampPoints,
|
brushStampPoints,
|
||||||
|
chooseCoordinateGuideStep,
|
||||||
clampCanvasView,
|
clampCanvasView,
|
||||||
clampFloatingButton,
|
clampFloatingButton,
|
||||||
|
coordinateGuideTicks,
|
||||||
linePoints,
|
linePoints,
|
||||||
normalizeBrushSize,
|
normalizeBrushSize,
|
||||||
restoreCanvasToolState,
|
restoreCanvasToolState,
|
||||||
@@ -31,8 +33,9 @@ import {
|
|||||||
} from "../scene-board.js";
|
} from "../scene-board.js";
|
||||||
|
|
||||||
const canvas = getBoardOverlay();
|
const canvas = getBoardOverlay();
|
||||||
const localState = createLocalState("canvas-tools", 2);
|
const localState = createLocalState("canvas-tools", 3);
|
||||||
const previousLocalState = createLocalState("canvas-tools", 1).load(null);
|
const previousLocalState = createLocalState("canvas-tools", 2).load(null)
|
||||||
|
|| createLocalState("canvas-tools", 1).load(null);
|
||||||
const legacyState = createLocalState("canvas", 3).load(null)
|
const legacyState = createLocalState("canvas", 3).load(null)
|
||||||
|| createLocalState("canvas", 2).load(null)
|
|| createLocalState("canvas", 2).load(null)
|
||||||
|| {};
|
|| {};
|
||||||
@@ -45,12 +48,15 @@ const pixelEditToggle = document.getElementById("pixel-edit-toggle");
|
|||||||
const pixelEditStatus = document.getElementById("pixel-edit-status");
|
const pixelEditStatus = document.getElementById("pixel-edit-status");
|
||||||
const pixelGrid = document.getElementById("pixel-grid");
|
const pixelGrid = document.getElementById("pixel-grid");
|
||||||
const boardStage = document.getElementById("board-stage");
|
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 canvasWorkbench = document.querySelector(".canvas-workbench");
|
||||||
const startEditButton = document.getElementById("start-canvas-edit");
|
const startEditButton = document.getElementById("start-canvas-edit");
|
||||||
const assistive = document.getElementById("canvas-edit-assistive");
|
const assistive = document.getElementById("canvas-edit-assistive");
|
||||||
const assistiveToggle = document.getElementById("canvas-assistive-toggle");
|
const assistiveToggle = document.getElementById("canvas-assistive-toggle");
|
||||||
const assistiveMenu = document.getElementById("canvas-assistive-menu");
|
const assistiveMenu = document.getElementById("canvas-assistive-menu");
|
||||||
const editorModeHint = document.getElementById("canvas-edit-mode-hint");
|
const editorModeHint = document.getElementById("canvas-edit-mode-hint");
|
||||||
|
const coordinateGuideToggle = document.getElementById("toggle-canvas-coordinates");
|
||||||
const undoButton = document.getElementById("undo-canvas-edit");
|
const undoButton = document.getElementById("undo-canvas-edit");
|
||||||
const exitEditButton = document.getElementById("exit-canvas-edit");
|
const exitEditButton = document.getElementById("exit-canvas-edit");
|
||||||
const moveViewButton = document.getElementById("move-canvas-view");
|
const moveViewButton = document.getElementById("move-canvas-view");
|
||||||
@@ -61,6 +67,7 @@ let previousDrawingTool = tool;
|
|||||||
let pixelEditEnabled = savedTools.pixelEditEnabled;
|
let pixelEditEnabled = savedTools.pixelEditEnabled;
|
||||||
let normalBrushSize = savedTools.normalBrushSize;
|
let normalBrushSize = savedTools.normalBrushSize;
|
||||||
let pixelBrushSize = savedTools.pixelBrushSize;
|
let pixelBrushSize = savedTools.pixelBrushSize;
|
||||||
|
let coordinateGuideEnabled = savedTools.coordinateGuideEnabled;
|
||||||
let drawing = false;
|
let drawing = false;
|
||||||
let drawingPointerId = null;
|
let drawingPointerId = null;
|
||||||
let lastPoint = null;
|
let lastPoint = null;
|
||||||
@@ -71,6 +78,7 @@ let view = { scale: 1, x: 0, y: 0 };
|
|||||||
let viewGesture = null;
|
let viewGesture = null;
|
||||||
let assistiveDrag = null;
|
let assistiveDrag = null;
|
||||||
let suppressAssistiveClick = false;
|
let suppressAssistiveClick = false;
|
||||||
|
let coordinateGuideFrame = null;
|
||||||
let assistivePosition = { x: 36, y: window.innerHeight / 2 };
|
let assistivePosition = { x: 36, y: window.innerHeight / 2 };
|
||||||
const viewPointers = new Map();
|
const viewPointers = new Map();
|
||||||
const undoHistory = new PixelUndoHistory();
|
const undoHistory = new PixelUndoHistory();
|
||||||
@@ -148,11 +156,130 @@ function viewportBounds() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function applyCanvasView(next = view) {
|
||||||
view = clampCanvasView(next, viewportBounds());
|
view = clampCanvasView(next, viewportBounds());
|
||||||
boardStage.style.setProperty("--canvas-view-scale", String(view.scale));
|
boardStage.style.setProperty("--canvas-view-scale", String(view.scale));
|
||||||
boardStage.style.setProperty("--canvas-view-x", `${view.x}px`);
|
boardStage.style.setProperty("--canvas-view-x", `${view.x}px`);
|
||||||
boardStage.style.setProperty("--canvas-view-y", `${view.y}px`);
|
boardStage.style.setProperty("--canvas-view-y", `${view.y}px`);
|
||||||
|
scheduleCoordinateGuideRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetCanvasView() {
|
function resetCanvasView() {
|
||||||
@@ -254,9 +381,11 @@ function enterCanvasEditor() {
|
|||||||
assistive.hidden = false;
|
assistive.hidden = false;
|
||||||
closeAssistiveMenu();
|
closeAssistiveMenu();
|
||||||
updateEditorModeControls();
|
updateEditorModeControls();
|
||||||
|
updateCoordinateGuideControl();
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
resetCanvasView();
|
resetCanvasView();
|
||||||
resetAssistivePosition();
|
resetAssistivePosition();
|
||||||
|
scheduleCoordinateGuideRender();
|
||||||
assistiveToggle.focus();
|
assistiveToggle.focus();
|
||||||
});
|
});
|
||||||
announce("已进入全屏画布编辑;左侧悬浮按钮可撤销、移动视线或退出");
|
announce("已进入全屏画布编辑;左侧悬浮按钮可撤销、移动视线或退出");
|
||||||
@@ -271,6 +400,7 @@ function exitCanvasEditor({ returnToTools = true } = {}) {
|
|||||||
viewGesture = null;
|
viewGesture = null;
|
||||||
closeAssistiveMenu();
|
closeAssistiveMenu();
|
||||||
assistive.hidden = true;
|
assistive.hidden = true;
|
||||||
|
hideCoordinateGuide();
|
||||||
delete canvas.dataset.canvasEditing;
|
delete canvas.dataset.canvasEditing;
|
||||||
document.body.classList.remove("canvas-edit-active");
|
document.body.classList.remove("canvas-edit-active");
|
||||||
delete document.body.dataset.canvasEditorMode;
|
delete document.body.dataset.canvasEditorMode;
|
||||||
@@ -307,9 +437,21 @@ function persistTools() {
|
|||||||
pixelEditEnabled,
|
pixelEditEnabled,
|
||||||
normalBrushSize,
|
normalBrushSize,
|
||||||
pixelBrushSize,
|
pixelBrushSize,
|
||||||
|
coordinateGuideEnabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleCoordinateGuide() {
|
||||||
|
coordinateGuideEnabled = !coordinateGuideEnabled;
|
||||||
|
updateCoordinateGuideControl();
|
||||||
|
persistTools();
|
||||||
|
scheduleCoordinateGuideRender();
|
||||||
|
closeAssistiveMenu({ restoreFocus: true });
|
||||||
|
announce(coordinateGuideEnabled
|
||||||
|
? "已显示画布坐标系,标尺和辅助线会随视角缩放"
|
||||||
|
: "已隐藏画布坐标系");
|
||||||
|
}
|
||||||
|
|
||||||
function renderBrushSize() {
|
function renderBrushSize() {
|
||||||
const mode = brushMode();
|
const mode = brushMode();
|
||||||
const brushSize = currentBrushSize();
|
const brushSize = currentBrushSize();
|
||||||
@@ -477,6 +619,7 @@ backgroundColor.addEventListener("input", persistTools);
|
|||||||
brushSizeInput.addEventListener("input", () => setBrushSize(brushSizeInput.value));
|
brushSizeInput.addEventListener("input", () => setBrushSize(brushSizeInput.value));
|
||||||
brushSizePresets.forEach((button) => button.addEventListener("click", () => setBrushSize(button.dataset.brushSize)));
|
brushSizePresets.forEach((button) => button.addEventListener("click", () => setBrushSize(button.dataset.brushSize)));
|
||||||
pixelEditToggle.addEventListener("click", togglePixelEditMode);
|
pixelEditToggle.addEventListener("click", togglePixelEditMode);
|
||||||
|
coordinateGuideToggle.addEventListener("click", toggleCoordinateGuide);
|
||||||
document.getElementById("fill-canvas").addEventListener("click", () => {
|
document.getElementById("fill-canvas").addEventListener("click", () => {
|
||||||
const before = pixelSnapshot();
|
const before = pixelSnapshot();
|
||||||
const rgb = hexToRgb(backgroundColor.value);
|
const rgb = hexToRgb(backgroundColor.value);
|
||||||
@@ -563,6 +706,7 @@ refreshColorTrigger(paintColor);
|
|||||||
refreshColorTrigger(backgroundColor);
|
refreshColorTrigger(backgroundColor);
|
||||||
setActiveButton(toolButtons, toolButtons.find((button) => button.dataset.canvasTool === tool));
|
setActiveButton(toolButtons, toolButtons.find((button) => button.dataset.canvasTool === tool));
|
||||||
renderBrushSize();
|
renderBrushSize();
|
||||||
|
updateCoordinateGuideControl();
|
||||||
persistTools();
|
persistTools();
|
||||||
updateEditorModeControls();
|
updateEditorModeControls();
|
||||||
|
|
||||||
@@ -581,5 +725,6 @@ registerWorkspace({
|
|||||||
exitCanvasEditor({ returnToTools: false });
|
exitCanvasEditor({ returnToTools: false });
|
||||||
finishDrawing();
|
finishDrawing();
|
||||||
pixelGrid.hidden = true;
|
pixelGrid.hidden = true;
|
||||||
|
hideCoordinateGuide();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import {
|
|||||||
PixelUndoHistory,
|
PixelUndoHistory,
|
||||||
brushModeSpec,
|
brushModeSpec,
|
||||||
brushStampPoints,
|
brushStampPoints,
|
||||||
|
chooseCoordinateGuideStep,
|
||||||
clampCanvasView,
|
clampCanvasView,
|
||||||
clampFloatingButton,
|
clampFloatingButton,
|
||||||
|
coordinateGuideTicks,
|
||||||
linePoints,
|
linePoints,
|
||||||
normalizeBrushSize,
|
normalizeBrushSize,
|
||||||
restoreCanvasToolState,
|
restoreCanvasToolState,
|
||||||
@@ -60,7 +62,7 @@ test("line interpolation includes endpoints without coordinate gaps", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("v1 tools migrate into normal mode while v2 restores both sizes", () => {
|
test("older tools migrate while v3 restores the coordinate guide choice", () => {
|
||||||
assert.deepEqual(restoreCanvasToolState(null, {
|
assert.deepEqual(restoreCanvasToolState(null, {
|
||||||
paintColor: "#123456",
|
paintColor: "#123456",
|
||||||
backgroundColor: "#654321",
|
backgroundColor: "#654321",
|
||||||
@@ -73,12 +75,14 @@ test("v1 tools migrate into normal mode while v2 restores both sizes", () => {
|
|||||||
pixelEditEnabled: false,
|
pixelEditEnabled: false,
|
||||||
normalBrushSize: 8,
|
normalBrushSize: 8,
|
||||||
pixelBrushSize: 1,
|
pixelBrushSize: 1,
|
||||||
|
coordinateGuideEnabled: false,
|
||||||
});
|
});
|
||||||
assert.deepEqual(restoreCanvasToolState({
|
assert.deepEqual(restoreCanvasToolState({
|
||||||
tool: "brush",
|
tool: "brush",
|
||||||
pixelEditEnabled: true,
|
pixelEditEnabled: true,
|
||||||
normalBrushSize: 12,
|
normalBrushSize: 12,
|
||||||
pixelBrushSize: 4,
|
pixelBrushSize: 4,
|
||||||
|
coordinateGuideEnabled: true,
|
||||||
}), {
|
}), {
|
||||||
paintColor: null,
|
paintColor: null,
|
||||||
backgroundColor: null,
|
backgroundColor: null,
|
||||||
@@ -86,8 +90,58 @@ test("v1 tools migrate into normal mode while v2 restores both sizes", () => {
|
|||||||
pixelEditEnabled: true,
|
pixelEditEnabled: true,
|
||||||
normalBrushSize: 12,
|
normalBrushSize: 12,
|
||||||
pixelBrushSize: 4,
|
pixelBrushSize: 4,
|
||||||
|
coordinateGuideEnabled: true,
|
||||||
});
|
});
|
||||||
assert.equal(restoreCanvasToolState("damaged", { brushSize: "bad" }).normalBrushSize, 1);
|
assert.equal(restoreCanvasToolState("damaged", { brushSize: "bad" }).normalBrushSize, 1);
|
||||||
|
assert.equal(restoreCanvasToolState("damaged", { coordinateGuideEnabled: true }).coordinateGuideEnabled, false);
|
||||||
|
assert.deepEqual(restoreCanvasToolState(null, {
|
||||||
|
pixelEditEnabled: true,
|
||||||
|
normalBrushSize: 9,
|
||||||
|
pixelBrushSize: 3,
|
||||||
|
}), {
|
||||||
|
paintColor: null,
|
||||||
|
backgroundColor: null,
|
||||||
|
tool: "brush",
|
||||||
|
pixelEditEnabled: true,
|
||||||
|
normalBrushSize: 9,
|
||||||
|
pixelBrushSize: 3,
|
||||||
|
coordinateGuideEnabled: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("coordinate guide spacing adapts to the transformed logical pixel size", () => {
|
||||||
|
assert.equal(chooseCoordinateGuideStep(4.625), 8);
|
||||||
|
assert.equal(chooseCoordinateGuideStep(9.25), 4);
|
||||||
|
assert.equal(chooseCoordinateGuideStep(18.5), 2);
|
||||||
|
assert.equal(chooseCoordinateGuideStep(37), 1);
|
||||||
|
assert.equal(chooseCoordinateGuideStep("damaged"), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("coordinate guide ticks preserve logical values through zoom and pan", () => {
|
||||||
|
assert.deepEqual(coordinateGuideTicks({
|
||||||
|
boardStart: 20,
|
||||||
|
boardSize: 320,
|
||||||
|
visibleStart: 0,
|
||||||
|
visibleEnd: 400,
|
||||||
|
step: 16,
|
||||||
|
}), [
|
||||||
|
{ value: 0, position: 20 },
|
||||||
|
{ value: 16, position: 100 },
|
||||||
|
{ value: 32, position: 180 },
|
||||||
|
{ value: 48, position: 260 },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(coordinateGuideTicks({
|
||||||
|
boardStart: -300,
|
||||||
|
boardSize: 640,
|
||||||
|
visibleStart: 0,
|
||||||
|
visibleEnd: 320,
|
||||||
|
step: 8,
|
||||||
|
}), [
|
||||||
|
{ value: 32, position: 20 },
|
||||||
|
{ value: 40, position: 100 },
|
||||||
|
{ value: 48, position: 180 },
|
||||||
|
{ value: 56, position: 260 },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pixel mode markup and grid stay accessible and non-interactive", async () => {
|
test("pixel mode markup and grid stay accessible and non-interactive", async () => {
|
||||||
@@ -168,13 +222,19 @@ test("canvas editor markup, guarded input, and checkpoint events stay wired", as
|
|||||||
]);
|
]);
|
||||||
assert.match(index, /id="start-canvas-edit"[^>]*>开始编辑<\/button>/);
|
assert.match(index, /id="start-canvas-edit"[^>]*>开始编辑<\/button>/);
|
||||||
assert.match(index, /id="canvas-assistive-toggle"[^>]+aria-expanded="false"/);
|
assert.match(index, /id="canvas-assistive-toggle"[^>]+aria-expanded="false"/);
|
||||||
|
assert.match(index, /id="canvas-coordinate-guide"[^>]+aria-hidden="true"[^>]+hidden/);
|
||||||
|
assert.match(index, /id="toggle-canvas-coordinates"[^>]+aria-pressed="false"/);
|
||||||
assert.match(index, /id="undo-canvas-edit"[^>]+aria-disabled="true"/);
|
assert.match(index, /id="undo-canvas-edit"[^>]+aria-disabled="true"/);
|
||||||
assert.match(index, /id="move-canvas-view"/);
|
assert.match(index, /id="move-canvas-view"/);
|
||||||
assert.match(index, /id="continue-canvas-edit"/);
|
assert.match(index, /id="continue-canvas-edit"/);
|
||||||
assert.match(style, /body\.canvas-edit-active #board-shell\s*\{[^}]*position:\s*fixed/s);
|
assert.match(style, /body\.canvas-edit-active #board-shell\s*\{[^}]*position:\s*fixed/s);
|
||||||
|
assert.match(style, /\.canvas-coordinate-guide\s*\{[^}]*pointer-events:\s*none/s);
|
||||||
|
assert.match(style, /body\.canvas-edit-active \.canvas-coordinate-guide:not\(\[hidden\]\)\s*\{[^}]*z-index:\s*1005/s);
|
||||||
assert.match(style, /#scene-overlay\[data-interaction="pixels"\]:not\(\[data-canvas-editing="true"\]\)\s*\{[^}]*pointer-events:\s*none/s);
|
assert.match(style, /#scene-overlay\[data-interaction="pixels"\]:not\(\[data-canvas-editing="true"\]\)\s*\{[^}]*pointer-events:\s*none/s);
|
||||||
assert.match(canvasView, /if \(!editorActive \|\| getInteractionMode\(\) !== "pixels"\) return/);
|
assert.match(canvasView, /if \(!editorActive \|\| getInteractionMode\(\) !== "pixels"\) return/);
|
||||||
assert.match(canvasView, /new PixelUndoHistory\(\)/);
|
assert.match(canvasView, /new PixelUndoHistory\(\)/);
|
||||||
|
assert.match(canvasView, /createLocalState\("canvas-tools", 3\)/);
|
||||||
|
assert.match(canvasView, /coordinateGuide\.hidden = true/);
|
||||||
assert.match(sceneBoard, /matrix:scene-replaced/);
|
assert.match(sceneBoard, /matrix:scene-replaced/);
|
||||||
assert.match(templates, /matrix:canvas-history-checkpoint/);
|
assert.match(templates, /matrix:canvas-history-checkpoint/);
|
||||||
assert.match(animations, /matrix:canvas-history-checkpoint/);
|
assert.match(animations, /matrix:canvas-history-checkpoint/);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
| `TEST-FRP-MAINTENANCE` | `WEB-FRP-MAINTENANCE`、`CONFIG-FRP`、`DEPLOY-FRP`、多配置、systemd 持久与真实 SSH 穿透 | Windows mock + 核桃派 | 否 |
|
| `TEST-FRP-MAINTENANCE` | `WEB-FRP-MAINTENANCE`、`CONFIG-FRP`、`DEPLOY-FRP`、多配置、systemd 持久与真实 SSH 穿透 | Windows mock + 核桃派 | 否 |
|
||||||
| `TEST-NETWORK-DIAGNOSTICS` | `WEB-NETWORK-DIAGNOSTICS`、接口、路由、DNS、TCP/TLS 和 ICMP 分类 | Windows mock + 核桃派 | 否 |
|
| `TEST-NETWORK-DIAGNOSTICS` | `WEB-NETWORK-DIAGNOSTICS`、接口、路由、DNS、TCP/TLS 和 ICMP 分类 | Windows mock + 核桃派 | 否 |
|
||||||
| `TEST-WS-CANVAS` | RGB888、最新帧覆盖、正常断开 | mock / 核桃派 | 否 |
|
| `TEST-WS-CANVAS` | RGB888、最新帧覆盖、正常断开 | mock / 核桃派 | 否 |
|
||||||
| `TEST-CANVAS-EDIT` | `WEB-CANVAS-EDIT` 默认只读、全屏编辑、悬浮菜单、撤销和平移缩放 | Windows / 本机浏览器 | 否 |
|
| `TEST-CANVAS-EDIT` | `WEB-CANVAS-EDIT`、`WEB-CANVAS-COORDINATES` 默认只读、全屏编辑、悬浮菜单、撤销、平移缩放和自适应坐标系 | Windows / 本机浏览器 | 否 |
|
||||||
| `TEST-TEMPLATE-MANAGEMENT` | `WEB-PREVIEW`、`WEB-TEMPLATES`、静态/动图播放编辑解耦、保存选择与未保存守卫 | Windows / mock / 核桃派 | 否 |
|
| `TEST-TEMPLATE-MANAGEMENT` | `WEB-PREVIEW`、`WEB-TEMPLATES`、静态/动图播放编辑解耦、保存选择与未保存守卫 | Windows / mock / 核桃派 | 否 |
|
||||||
| `TEST-CURRENT-PLAYBACK` | `WEB-CURRENT-DISPLAY`、当前帧弹层、动图跳转、暂停/继续、倍速和会话失效 | Windows / mock + 核桃派软件接口 | 用户最终交互确认 |
|
| `TEST-CURRENT-PLAYBACK` | `WEB-CURRENT-DISPLAY`、当前帧弹层、动图跳转、暂停/继续、倍速和会话失效 | Windows / mock + 核桃派软件接口 | 用户最终交互确认 |
|
||||||
| `TEST-ANIMATION-THUMBNAIL-PERF` | 动图长名称、完整动态缩略图、浏览器/HTTP 资源与 H618 GPIO 刷新隔离 | Windows / mock + 核桃派/HUB75 | 仅摄像头歧义时确认 |
|
| `TEST-ANIMATION-THUMBNAIL-PERF` | 动图长名称、完整动态缩略图、浏览器/HTTP 资源与 H618 GPIO 刷新隔离 | Windows / mock + 核桃派/HUB75 | 仅摄像头歧义时确认 |
|
||||||
@@ -59,7 +59,7 @@ node --test @($tests.FullName)
|
|||||||
|
|
||||||
`TEST-SYSTEM-INDICATOR-PIXELS` 在 mock 中逐像素验证 OTA 的 `OTA`、百分比和进度条缩小后无断笔、百分比未被底边裁切、四边仍各有 3 像素黑边;联网 IP 与断网提示的字体覆盖像素只能是全白或全灭,禁止抗锯齿灰阶造成低亮度缺块。部署后依次只显示 OTA、联网 IP、断网提示三个画面;每次只显示一个并停止,由用户回复内容无误后再进入下一个,不使用摄像头代替本轮人工确认,三个画面均确认前不得写成通过。
|
`TEST-SYSTEM-INDICATOR-PIXELS` 在 mock 中逐像素验证 OTA 的 `OTA`、百分比和进度条缩小后无断笔、百分比未被底边裁切、四边仍各有 3 像素黑边;联网 IP 与断网提示的字体覆盖像素只能是全白或全灭,禁止抗锯齿灰阶造成低亮度缺块。部署后依次只显示 OTA、联网 IP、断网提示三个画面;每次只显示一个并停止,由用户回复内容无误后再进入下一个,不使用摄像头代替本轮人工确认,三个画面均确认前不得写成通过。
|
||||||
|
|
||||||
`TEST-CANVAS-EDIT` 使用本机 mock 服务,在约 `320px`、`390px`、平板和桌面视口逐项检查:像素工作区默认滑动画板不落笔且页面可滚动;点击“开始编辑”后才进入全屏绘画;悬浮按钮点击与拖动不产生像素;移动模式支持单指平移、双指缩放和桌面滚轮缩放,切回绘画后落点准确;画笔、橡皮、填充和清空可连续撤销。退出编辑回到颜色工具区,离开工作区强制退出;文字工作区保持原有选择、拖动和缩放。成功应用画板、保存或更新模板、保存或新建动图帧后撤销历史清空,失败请求保留历史。该测试不连接 HUB75、ADC,也不进行实屏目测。
|
`TEST-CANVAS-EDIT` 使用本机 mock 服务,在约 `320px`、`390px`、平板和桌面视口逐项检查:像素工作区默认滑动画板不落笔且页面可滚动;点击“开始编辑”后才进入全屏绘画;悬浮按钮点击与拖动不产生像素;移动模式支持单指平移、双指缩放和桌面滚轮缩放,切回绘画后落点准确;画笔、橡皮、填充和清空可连续撤销。悬浮菜单的坐标系按钮使用 `aria-pressed`,默认关闭并按浏览器记忆;坐标层无指针事件,只在全屏像素编辑中显示。分别在 `1x/2x/4x/8x` 缩放和不同平移位置检查上、左 `0..63` 标尺、主刻度与贯穿线对齐,间隔从 `1/2/4/8/16/32` 中自适应;普通与单像素模式均可使用,单像素网格可同时显示。切换坐标系前后重复绘画、吸管、撤销、平移和缩放,并逐字节比较场景、导出 PNG 与 WebSocket RGB888 不变。退出编辑回到颜色工具区,离开工作区强制退出;文字工作区保持原有选择、拖动和缩放。成功应用画板、保存或更新模板、保存或新建动图帧后撤销历史清空,失败请求保留历史。该测试不连接 HUB75、ADC,也不进行实屏目测。
|
||||||
|
|
||||||
`TEST-TEMPLATE-MANAGEMENT` 使用 mock 和前端自动化验证:模板管理顶部为与系统设置相同的 `160 CSS px` 当前帧监视器且没有应用、导出或画板交互;静态模板和动图卡片均按“播放/编辑、复制/重命名、删除、上移/下移”两列四行排列,演示案例显示相同布局并对受限项提供原因。静态与动图播放只改变设备输出,不替换普通画布或动图编辑上下文,也不修改默认内容;静态编辑进入像素画布,未绑定草稿或保存后又修改的模板在被替换前提示“当前内容将会丢失”,干净模板直接切换,取消及读取失败保持原状态。普通模板保存覆盖当前或另存后都更新绑定基线,冲突保留未保存内容;演示静态图只能另存。板端只用故意过期的静态播放修订验证 `409` 且当前帧字节不变,不要求摄像头、实屏目测或物理操作。
|
`TEST-TEMPLATE-MANAGEMENT` 使用 mock 和前端自动化验证:模板管理顶部为与系统设置相同的 `160 CSS px` 当前帧监视器且没有应用、导出或画板交互;静态模板和动图卡片均按“播放/编辑、复制/重命名、删除、上移/下移”两列四行排列,演示案例显示相同布局并对受限项提供原因。静态与动图播放只改变设备输出,不替换普通画布或动图编辑上下文,也不修改默认内容;静态编辑进入像素画布,未绑定草稿或保存后又修改的模板在被替换前提示“当前内容将会丢失”,干净模板直接切换,取消及读取失败保持原状态。普通模板保存覆盖当前或另存后都更新绑定基线,冲突保留未保存内容;演示静态图只能另存。板端只用故意过期的静态播放修订验证 `409` 且当前帧字节不变,不要求摄像头、实屏目测或物理操作。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user