diff --git a/整体开发需求/01_网页配置端需求.md b/整体开发需求/01_网页配置端需求.md index 0d06def..7dc91e2 100644 --- a/整体开发需求/01_网页配置端需求.md +++ b/整体开发需求/01_网页配置端需求.md @@ -44,6 +44,7 @@ | `WEB-DIAGNOSTIC` | 硬件诊断图案 | 支持发送四角横线、行带、地址检查和文字复现图案,用于排查重复/错位显示。 | | `WEB-CANVAS` | 像素底层和 WebSocket 帧 | 维护 `64 x 64 x RGB` 像素底层,合成后通过 `WS /ws/canvas` 发送最终整帧。 | | `WEB-CANVAS-EDIT` | 防误触全屏编辑与画布视角 | 像素画布默认只读,经明确入口进入全屏绘画;编辑期提供可移动悬浮菜单、会话撤销以及独立的平移缩放模式。 | +| `WEB-CANVAS-COORDINATES` | 自适应画布坐标系 | 全屏像素编辑可从悬浮菜单开关浏览器本地记忆的坐标标尺与贯穿辅助线,随平移和缩放自适应且不进入任何场景或输出。 | | `WEB-COLOR-UI` | 统一颜色选择与绘画工具 | 使用软件自有的 RGB/HSV 取色面板、最近颜色、共享色板和触摸友好的画笔工具,不调用系统颜色选择器。 | | `WEB-ORIENTATION` | 页面方向设置 | 支持 `0/90/180/270`,保存后重启仍生效。 | | `WEB-BRIGHTNESS` | 页面实时亮度设置 | 拖动亮度条时持续应用并保存最新值,当前画面立即反馈且不需要再次发送。 | @@ -451,6 +452,10 @@ WantedBy=multi-user.target - 共享画板在像素工作区默认只读,触摸不会落笔或阻止页面滚动;颜色和画笔工具区提供明确的“开始编辑”按钮,点击后使用现有共享画板进入 CSS 全视口专注编辑,不调用浏览器 Fullscreen API。 - 专注编辑默认进入绘画模式并锁定页面滚动;退出时完成已改变的当前笔画、复位视角并回到颜色和画笔工具区。离开像素工作区也必须先安全退出。文字工作区的画板选择、拖动和缩放行为不受该状态影响。 - 专注编辑内持续显示类似 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` 内缩放,桌面滚轮按指针位置缩放;切回继续编辑后保留视角并按变换后的画板矩形换算逻辑坐标。视口变化后重新约束位置,画布不得完全移出可见区。 - 撤销历史按每一笔画笔/橡皮、填充背景和清空像素层分组,只在像素确实变化时记录操作前的 RGB 快照;最多 50 步,只存在当前页面内存。撤销仅恢复像素层并保留文字。退出再进入专注编辑不清空历史;页面刷新、场景替换,以及成功应用统一画板、保存或更新模板、保存或新建动图帧后清空,失败提交不得清空。 - 支持“应用统一画板”和“保存 PNG”;PNG 保存导出最终组合场景,不只导出像素底层。 @@ -501,6 +506,7 @@ WebSocket 消息示例: - 画布快速拖动时服务不崩溃,最多丢弃旧帧。 - 单像素模式的 `1x1/2x2/3x3/4x4` 笔尖均为完整正方形,快速拖动不留采样断线,边缘按 `0..63` 精确裁剪;网格在深浅像素上可辨认且不改变导出或发送的 RGB。 - 约 `320px` 和常见手机视口下,未进入编辑时在画板上滑动页面不会改变像素;进入专注编辑后悬浮按钮拖动不落笔,移动模式可平移缩放,继续编辑后的落点仍与视觉像素一致。 +- 坐标系在约 `320px`、`390px`、平板和桌面视口以及 `1x/2x/4x/8x` 视角下保持刻度、数字和贯穿线对齐,缩放时自动调整密度;普通和单像素绘画、吸管、撤销、平移、缩放及最终 PNG/RGB888 在开关前后保持一致。 - 撤销可连续恢复所有未提交像素操作;成功提交当前场景后撤销立即不可用,失败提交仍可撤销。历史不写入浏览器持久存储、设备配置、模板或动图记录。 ### 2.3.1 统一颜色选择器(`WEB-COLOR-UI`、`CONFIG-COLOR-PALETTE`) @@ -1143,7 +1149,7 @@ WebSocket 消息示例: - 只有新内容已完整校验并成功写入稳定键后,才精确删除本次已迁移的旧键;写入失败时旧键和值必须保留。 - 稳定键内容损坏、出现未知更高版本或迁移失败时,页面保留原值和内存草稿、停止自动写回并明确提示用户;不得用空白场景覆盖,不得自动上传到核桃派。场景只保存在当前浏览器,不写入设备 `config.json`,不要求跨浏览器共享,也不保证核桃派重启后自动恢复实屏。 - 同一来源的其他标签页修改稳定场景键时不得直接替换当前内存草稿;冲突解决前当前标签页的后续编辑继续保留在内存但暂停写回 `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,不得把未来场景大对象无条件塞入服务端配置。 核桃派模板(`CONFIG-TEMPLATES`)使用持久根下独立的 `templates/` 数据区: diff --git a/核桃派软件源代码/FEATURE_UPDATED_AT b/核桃派软件源代码/FEATURE_UPDATED_AT index a4c75e5..478e45e 100644 --- a/核桃派软件源代码/FEATURE_UPDATED_AT +++ b/核桃派软件源代码/FEATURE_UPDATED_AT @@ -1 +1 @@ -2026-09-08T10:19+08:00 +2026-09-10T11:10+08:00 diff --git a/核桃派软件源代码/app/static/canvas-tools-model.js b/核桃派软件源代码/app/static/canvas-tools-model.js index b86bdf0..dbb64af 100644 --- a/核桃派软件源代码/app/static/canvas-tools-model.js +++ b/核桃派软件源代码/app/static/canvas-tools-model.js @@ -3,6 +3,8 @@ export const BRUSH_MODE_PIXEL = "pixel"; export const CANVAS_VIEW_MIN_SCALE = 1; export const CANVAS_VIEW_MAX_SCALE = 8; 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({ [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)); } -export function restoreCanvasToolState(savedV2, savedV1 = null) { - const current = record(savedV2); - const legacy = record(savedV1); +export function restoreCanvasToolState(savedV3, savedV2 = null) { + const current = record(savedV3); + const legacy = record(savedV2); const source = current || legacy || {}; return { paintColor: typeof source.paintColor === "string" ? source.paintColor : null, backgroundColor: typeof source.backgroundColor === "string" ? source.backgroundColor : null, tool: source.tool === "eraser" ? "eraser" : "brush", - pixelEditEnabled: Boolean(current?.pixelEditEnabled), + pixelEditEnabled: Boolean(source.pixelEditEnabled), normalBrushSize: normalizeBrushSize( - current?.normalBrushSize ?? source.brushSize, + source.normalBrushSize ?? source.brushSize, 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) { const points = []; let x = from.x; diff --git a/核桃派软件源代码/app/static/index.html b/核桃派软件源代码/app/static/index.html index aeb33fb..aafcece 100644 --- a/核桃派软件源代码/app/static/index.html +++ b/核桃派软件源代码/app/static/index.html @@ -104,6 +104,7 @@ +

正在恢复画板草稿…

@@ -118,6 +119,7 @@