Merge branch 'sub_laptop_3' of https://gitea.chemifactory.com/eomsangdon/Aislo into main_laptop_1

This commit is contained in:
2026-10-20 19:52:23 +09:00
9 changed files with 45 additions and 96 deletions
@@ -116,14 +116,22 @@ function buildScaleFields(
void save({ x: scaleToSend(x, fields[0].fallback), y: scaleToSend(y, fields[1].fallback) });
},
});
// 칸 밑 오류 글은 숨기고(틀린 칸만 길어짐) 줄 아래 한 줄로 알림
const alertLine = document.createElement("p");
alertLine.className = "b07-scale__error";
alertLine.setAttribute("role", "alert");
alertLine.hidden = true;
const check = (): void => {
let bad = false;
const errors = new Map<string, string[]>();
for (const f of fields) {
const result = f.read();
f.field.setError(typeof result === "string" ? result : undefined);
bad ||= typeof result === "string";
const message = typeof result === "string" ? result : undefined;
f.field.setError(message);
if (message) errors.set(message, [...(errors.get(message) ?? []), f.axis.toUpperCase()]);
}
saveButton.disabled = bad;
alertLine.textContent = [...errors].map(([m, axes]) => `${axes.join(" · ")}: ${m}`).join(" / ");
alertLine.hidden = errors.size === 0;
saveButton.disabled = errors.size > 0;
};
for (const f of fields) {
f.field.input.addEventListener("input", check);
@@ -153,5 +161,5 @@ function buildScaleFields(
const buttons = document.createElement("div");
buttons.className = "b07-scale__actions";
buttons.append(reset, saveButton);
section.append(title, row, buttons);
section.append(title, row, alertLine, buttons);
}
+11 -1
View File
@@ -480,7 +480,7 @@ html.is-bm-route .b07-cad-stack > .cad-tabs {
top: 0;
right: var(--cad-tab-slot-right, 0px);
left: var(--cad-tab-slot-left, 0px);
height: 26px;
height: 25px;
padding: 2px 4px 0;
border-bottom: none;
background: transparent;
@@ -522,6 +522,16 @@ html.is-bm-route .b07-cad-stack > .cad-tabs {
flex: 1;
}
.b07-scale .ui-field__error {
display: none;
}
.b07-scale__error {
margin: 0;
color: var(--color-danger);
font-size: 12px;
}
.b07-scale__actions {
display: flex;
justify-content: flex-end;
+1 -1
View File
@@ -105,7 +105,7 @@ body > canvas[data-id="canvas"] {
gap: 2px;
height: 26px;
padding: 0 8px;
border-bottom: 1px solid var(--cad-line);
border-bottom: 1px solid var(--color-smoke);
}
.cad-ribbon-tabs__slot {
flex: 1;
@@ -19,10 +19,7 @@ export const QuickAccessBar: FC = () => {
key={command.id}
hidden={QUICK_ACCESS_HIDDEN.includes(id)}
title={`${command.label} (${command.id})`}
onClick={(event) => {
event.currentTarget.blur();
runCommand(command);
}}
onClick={() => runCommand(command)}
>
<span aria-hidden>{command.glyph}</span>
</button>
@@ -1,29 +0,0 @@
/* 손가락 기기 명령 단추(기기대응 4-1 · `input-touch.ts`) — 자판 없이 Esc · Enter · 지움 · 되돌리기 · 다시 · 스냅.
자판 없는 기기(손가락이 주 입력 · 마우스 스침 없음)만 보임 — 터치 노트북엔 안 보임(요구 280-6). 그림 면 오른쪽 가운데에 세로로. */
.cad-touch-bar {
display: none;
}
@media (hover: none) and (pointer: coarse) {
.cad-touch-bar {
position: fixed;
top: 50%;
right: 8px;
z-index: 50;
display: flex;
flex-direction: column;
gap: 6px;
transform: translateY(-50%);
}
.cad-touch-bar > button {
width: 44px;
height: 44px;
border: 1px solid var(--color-border);
border-radius: 8px;
background: var(--color-surface-raised);
color: var(--color-text);
font-size: 18px;
box-shadow: var(--shadow-sm);
}
}
@@ -2,7 +2,6 @@ import { Point } from '@flatten-js/core';
import { setTouchPickPx } from '../helpers/pick-radius.ts';
import { getCanvas, getScreenCanvasDrawController } from '../state.ts';
import type { InputController } from './input-controller.ts';
import './input-touch.css';
/**
* 손가락 CAD(기기대응 4-1) — 마우스 코드는 그대로 두고 손가락만 여기서 번역한다.
@@ -11,7 +10,6 @@ import './input-touch.css';
* · 한 손가락 끌기 = 화면 옮기기 · 두 손가락 = 벌려 확대 + 같이 끌어 옮기기
* · 길게 누름(450ms) = 정밀 고르기 — 손가락 위 70px 에 십자선 · 끌어 맞추고 떼면 그 자리 클릭
* · 고름 반경 = 접촉 크기(최소 24px) · 마우스로 누르면 원래 반경
* · 자판 없이 쓰는 명령 단추(Esc · Enter · 지움 · 되돌리기 · 다시 · 스냅) — 손가락 기기에서만 보임(App.css)
*/
const LONG_MS = 450;
const SLOP_PX = 10;
@@ -115,30 +113,4 @@ export function attachTouch(ctl: InputController): void {
};
canvas.addEventListener('pointerup', (e) => lift(e, false));
canvas.addEventListener('pointercancel', (e) => lift(e, true));
mountTouchBar(ctl);
}
/** 자판 없는 명령 단추 — 단축키와 같은 길(`handleKeyStroke`)로 보낸다 */
function mountTouchBar(ctl: InputController): void {
const bar = document.createElement('div');
bar.className = 'cad-touch-bar';
const keys: [string, string, KeyboardEventInit][] = [
['Esc', '취소(Esc)', { key: 'Escape' }],
['↵', '확정(Enter)', { key: 'Enter' }],
['🗑', '지움(Delete)', { key: 'Delete' }],
['↶', '되돌리기(Ctrl+Z)', { key: 'z', code: 'KeyZ', ctrlKey: true }],
['↷', '다시(Ctrl+Y)', { key: 'y', code: 'KeyY', ctrlKey: true }],
['⌖', '스냅 켬/끔(F3)', { key: 'F3' }],
];
for (const [label, title, init] of keys) {
const b = document.createElement('button');
b.type = 'button';
b.textContent = label;
b.title = title;
b.setAttribute('aria-label', title);
b.addEventListener('click', () => ctl.handleKeyStroke(new KeyboardEvent('keydown', init)));
bar.append(b);
}
document.body.append(bar);
}
@@ -35,11 +35,7 @@ const CommandButton: FC<CommandButtonProps> = ({ command, size, activeTool, onRu
title={`${command.label}${command.aliases?.length ? ` (${command.aliases[0]})` : ''}${
command.hint ? ` — ${command.hint}` : ''
}`}
onClick={(event) => {
// 초점 테두리가 남아 「선택」과 둘이 켜진 듯 보이지 않게(487)
event.currentTarget.blur();
onRun(command);
}}
onClick={() => onRun(command)}
>
<span className="cad-tool__glyph">{command.glyph}</span>
<span className="cad-tool__label">{command.label}</span>
@@ -139,8 +135,7 @@ const Panel: FC<{
type="button"
key={command.id}
title={command.hint}
onClick={(event) => {
event.currentTarget.blur();
onClick={() => {
setOverflowAt(null);
onRun(command);
}}
@@ -205,7 +200,16 @@ export const Ribbon: FC<RibbonProps> = ({ activeTool, widgets, onCommandRun }) =
};
return (
<nav ref={navRef} className="cad-ribbon controls" aria-label="CAD 리본" data-peek={peeking}>
<nav
ref={navRef}
className="cad-ribbon controls"
aria-label="CAD 리본"
data-peek={peeking}
onClick={(event) => {
// 마우스로 누른 단추만 초점을 놓음 — 뒤이은 Esc 에 초점 테두리가 안 뜸(키보드 Enter · Space 는 detail 0 이라 남음)
if (event.detail > 0) (event.target as HTMLElement).closest('button')?.blur();
}}
>
<div className="cad-ribbon-tabs">
{RIBBON_TABS.map((tab) => (
<button
+1 -1
View File
@@ -163,7 +163,7 @@ def test_탭_줄_겹판_약속():
block = block[: block.index("}")]
assert "right: var(--cad-tab-slot-right, 0px);" in block
assert "left: var(--cad-tab-slot-left, 0px);" in block
assert re.search(r"height:\s*26px", block)
assert re.search(r"height:\s*25px", block)
class _Cursor:
+6 -19
View File
@@ -1,6 +1,6 @@
"""B07 CAD 손가락(PLAN_기기대응 4-1 · `openwebcad/src/inputController/input-touch.ts`) — 빌드된 CAD 화면.
① 자판 없는 기기(hover:none · pointer:coarse) = 명령 단추 6개(44px) · 마우스 · 터치 노트북 = 안 보임(요구 280-6)
① 손가락 명령 단추(.cad-touch-bar)는 없음(요구 298-1) — 마우스 · 손가락 화면 모두 DOM 0
② 확대 막기(user-scalable=no) 없음
③ 캔버스 touch-action none · 두 손가락 벌림 · 한 손가락 끌기가 페이지를 확대 · 밀지 않음(페이지 오류 0)
"""
@@ -32,24 +32,15 @@ def _touch(cdp, kind, points):
@pytest.mark.skipif(not _up(), reason="sub1 화면 서버(5174) 꺼짐")
def test_cad_touch_bar_and_gestures():
def test_cad_no_touch_bar_and_gestures():
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(channel="msedge", headless=True)
mouse = browser.new_page(viewport={"width": 1440, "height": 900})
mouse.goto(URL)
mouse.wait_for_selector(".cad-touch-bar", state="attached", timeout=20000)
assert (
mouse.evaluate("getComputedStyle(document.querySelector('.cad-touch-bar')).display")
== "none"
)
# 터치 노트북(any-pointer: coarse · 주 입력 마우스) — 단추는 hover:none 일 때만(에뮬은 터치 노트북을 못 꾸밈)
media = mouse.evaluate(
"[...document.styleSheets].flatMap(s => [...s.cssRules]).filter(r => r.media"
" && [...r.cssRules].some(c => c.selectorText === '.cad-touch-bar')).map(r => r.media.mediaText)"
)
assert media == ["(hover: none) and (pointer: coarse)"], media
mouse.wait_for_selector("canvas[data-id=canvas]", timeout=20000)
assert mouse.evaluate("document.querySelector('.cad-touch-bar')") is None
ctx = browser.new_context(
viewport={"width": 1180, "height": 820}, has_touch=True, is_mobile=True
)
@@ -57,11 +48,8 @@ def test_cad_touch_bar_and_gestures():
errors = []
page.on("pageerror", lambda e: errors.append(str(e)))
page.goto(URL)
page.wait_for_selector(".cad-touch-bar", timeout=20000)
sizes = page.evaluate(
"[...document.querySelectorAll('.cad-touch-bar > button')].map(b => [b.offsetWidth, b.offsetHeight])"
)
assert len(sizes) == 6 and all(w >= 44 and h >= 44 for w, h in sizes), sizes
page.wait_for_selector("canvas[data-id=canvas]", timeout=20000)
assert page.evaluate("document.querySelector('.cad-touch-bar')") is None
assert "user-scalable" not in page.evaluate(
"document.querySelector('meta[name=viewport]').content"
)
@@ -86,7 +74,6 @@ def test_cad_touch_bar_and_gestures():
for i in range(1, 8):
_touch(cdp, "touchMove", [(cx - 30 - i * 10, cy), (cx + 30 + i * 10, cy)])
_touch(cdp, "touchEnd", [])
page.locator(".cad-touch-bar > button").first.tap() # Esc
page.wait_for_timeout(300)
assert page.evaluate("visualViewport.scale") == 1
assert page.evaluate("[scrollX, scrollY]") == [0, 0]