From ef56588a92fdf17173136af8fbdc34d3ae2df3e5 Mon Sep 17 00:00:00 2001 From: misdake Date: Wed, 25 Mar 2026 09:25:31 +0800 Subject: [PATCH 01/12] css changes. replaced icons to fit dark theme. --- dist/res/github.png | Bin 8189 -> 6223 bytes dist/res/refresh.png | Bin 1395 -> 1856 bytes dist/style.css | 373 ++++++++++++++++++++++++++++++++---- ts/App.ts | 13 +- ts/elements/TitleElement.ts | 2 +- 5 files changed, 346 insertions(+), 42 deletions(-) diff --git a/dist/res/github.png b/dist/res/github.png index 27527560919b7005c6b41469080c9cbc09589d12..d44393a070d940c0af64716cfce4173ebba13551 100644 GIT binary patch literal 6223 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 literal 8189 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 diff --git a/dist/res/refresh.png b/dist/res/refresh.png index 710f50618a8227dd7adebb98548477219f763389..2a757a12f23561c891e33691a9956ccf7617f7c3 100644 GIT binary patch literal 1856 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 Bbd>-A diff --git a/dist/style.css b/dist/style.css index 0d3c6a3..7b17abd 100644 --- a/dist/style.css +++ b/dist/style.css @@ -3,15 +3,49 @@ padding: 0; } -body, html { - background: black; +:root { + --primary-color: #3B82F6; + --secondary-color: #60A5FA; + --accent-color: #22D3EE; + --background-dark: #0F172A; + --surface-color: #1E293B; + --text-primary: #F1F5F9; + --text-secondary: #94A3B8; + --border-color: #334155; + --success-color: #10B981; + --warning-color: #F59E0B; +} + +body, +html { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); height: 100%; overflow: hidden; + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; + color: var(--text-primary); } select { min-width: 150px; - height: 20px; + height: 36px; + padding: 8px 12px; + background: var(--surface-color); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +select:hover { + border-color: var(--primary-color); +} + +select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); } .refreshButton { @@ -19,53 +53,119 @@ select { width: 20px; height: 20px; border: 0; + margin-left: 5px; + background: transparent url('res/refresh.png'); background-size: cover; - background-image: url('res/refresh.png'); + transition: transform 0.3s ease; +} + +.refreshButton:hover { + transform: rotate(180deg); } #selectPanel { position: absolute; - top: 5px; - left: 5px; + top: 15px; + left: 15px; z-index: 999; + background: rgba(30, 41, 59, 0.6); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + padding: 10px; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); } #cameraPanel { position: absolute; - bottom: 5px; - left: 5px; + bottom: 15px; + left: 15px; z-index: 999; + background: rgba(30, 41, 59, 0.6); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + padding: 10px; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + gap: 8px; } .zoomButton { - width: 36px; - height: 36px; + width: 40px; + height: 40px; + border-radius: 8px; + border: none; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + color: white; + font-size: 18px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); +} + +.zoomButton:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(59, 130, 246, 0.4); +} + +.zoomButton:active { + transform: translateY(0); } .zoomText { - color: rgba(255, 255, 255, 0.8); + color: var(--text-secondary); font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; } #imageSource { display: inline-block; - color: rgba(255, 255, 255, 0.8); + color: var(--text-secondary); font-size: 12px; } #panel { display: none; float: left; - width: 210px; + width: 240px; height: 100%; - background: white; + background: rgba(30, 41, 59, 0.95); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); flex-flow: column; + border-right: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); } #panelScroll { height: 100%; overflow-y: auto; flex: 1 1 auto; + padding: 10px; +} + +#panelScroll::-webkit-scrollbar { + width: 8px; +} + +#panelScroll::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; +} + +#panelScroll::-webkit-scrollbar-thumb { + background: var(--primary-color); + border-radius: 4px; +} + +#panelScroll::-webkit-scrollbar-thumb:hover { + background: var(--secondary-color); } #container { @@ -75,39 +175,131 @@ select { } .configButton { - /*margin:20px 5px;*/ + padding: 4px 8px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; + margin: 4px 4px; +} + +.configButton:hover { + transform: translateY(-1px); + box-shadow: 0 0 20px rgba(102, 126, 234, 0.4); +} + +.configButton:active { + transform: translateY(0); +} + +.configButton::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transform: translate(-50%, -50%); + transition: width 0.6s ease, height 0.6s ease; +} + +.configButton:active::after { + width: 300px; + height: 300px; } .configColorAlphaContainer { - margin: 5px 10px; + margin: 8px 0; + padding: 12px; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; } .colorAlphaContainerSplit { - width: 0px; + width: 0; display: inline-block; } .configColorButton { - width: 24px; - height: 24px; - border-radius: 12px; - border: 2px black solid; - margin: 2px 2px; + width: 14px; + height: 28px; + border: 1px solid transparent; + margin: 4px 0 4px 4px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; +} + +.configColorButton:hover { + transform: scale(1.1); + border-color: var(--primary-color); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); } .configAlphaButton { - width: 12px; - height: 24px; - border: 2px black solid; - margin: 2px 0 2px 2px; + width: 14px; + height: 28px; + border: 1px solid transparent; + margin: 4px 0 4px 4px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; +} + +.configAlphaButton:hover { + transform: scale(1.1); + border-color: var(--primary-color); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); } .configCheckbox { - /*margin:20px 5px;*/ + display: flex; + align-items: center; + gap: 8px; + margin: 8px 0; + cursor: pointer; +} + +.configCheckbox input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--primary-color); + cursor: pointer; } .configText { - /*margin:20px 5px;*/ + margin: 8px 0; + font-size: 14px; + color: var(--text-secondary); +} + +input[type="text"], +input[type="number"] { + width: 100%; + padding: 10px 12px; + background: var(--surface-color); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 14px; + transition: all 0.2s ease; + box-sizing: border-box; +} + +input[type="text"]:focus, +input[type="number"]:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); } #footer { @@ -119,19 +311,27 @@ select { flex-flow: row; justify-content: space-around; flex-wrap: wrap; - margin-bottom: 3px; + background: rgba(15, 23, 42, 0.8); + border-top: 1px solid rgba(255, 255, 255, 0.1); } #footer a { cursor: pointer; display: flex; flex-flow: row; -} -#footer a { + align-items: center; + color: var(--text-secondary); + margin: 4px 0; + padding: 8px 8px; + border-radius: 20px; + transition: all 0.2s ease; text-decoration: none; } -#footer a :hover { - text-decoration: underline; + +#footer a:hover { + background: var(--primary-color); + color: white; + text-decoration: none !important; } #footer span { @@ -142,14 +342,111 @@ select { } .footerIcon { - width: 24px; - height: 24px; + width: 20px; + height: 20px; + margin-right: 8px; + opacity: 0.8; +} + +.uploadHelper { + display: inline-flex; + align-items: center; + padding: 6px 12px; + background: transparent; + border: 1px solid var(--border-color); + border-radius: 20px; + color: var(--text-secondary); + text-decoration: none; + font-size: 13px; + transition: all 0.2s ease; +} + +.uploadHelper:hover { + border-color: var(--primary-color); + color: var(--primary-color); + background: rgba(59, 130, 246, 0.1); } #toast { position: absolute; - bottom: 0; - right: 0; + bottom: 20px; + right: 20px; font-size: 16px; color: white; -} \ No newline at end of file + background: linear-gradient(135deg, var(--success-color), #059669); + padding: 12px 24px; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(16, 185, 129, 0.3); + animation: slideIn 0.3s ease; + z-index: 1000; + display: none; +} + +#toast.visible { + display: block; +} + +#toast.hiding { + animation: slideOut 0.3s ease forwards; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + + to { + transform: translateX(100%); + opacity: 0; + } +} + +.label { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 6px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 1px; +} + +.separator { + height: 1px; + background: linear-gradient(90deg, transparent, var(--border-color), transparent); + margin: 16px 0; +} + +@media (max-width: 768px) { + #panel { + width: 200px; + } + + .configButton { + padding: 10px 16px; + font-size: 13px; + } + + #selectPanel, + #cameraPanel { + padding: 10px; + border-radius: 8px; + } + + .zoomButton { + width: 36px; + height: 36px; + } +} diff --git a/ts/App.ts b/ts/App.ts index 887d4cc..0f95544 100644 --- a/ts/App.ts +++ b/ts/App.ts @@ -167,11 +167,18 @@ function showToast(content: string) { let element = document.getElementById("toast"); if (element) { - element.style.display = "block"; + element.classList.remove("hiding"); + element.classList.add("visible"); element.innerText = content; + element.style.display = "block"; toastTimeout = setTimeout(() => { - element.style.display = "none"; - element.innerText = ""; + element.classList.remove("visible"); + element.classList.add("hiding"); + setTimeout(() => { + element.classList.remove("hiding"); + element.innerText = ""; + element.style.display = "none"; + }, 300); }, 2000); } } diff --git a/ts/elements/TitleElement.ts b/ts/elements/TitleElement.ts index 89432ce..8806a15 100644 --- a/ts/elements/TitleElement.ts +++ b/ts/elements/TitleElement.ts @@ -75,7 +75,7 @@ export class TitleElement extends LitElement {
- <-how? + ? `; } From 72b5a62ebf4e9862709a94d08707c85731021cec Mon Sep 17 00:00:00 2001 From: misdake Date: Wed, 25 Mar 2026 12:22:42 +0800 Subject: [PATCH 02/12] refactor: convert left panel to floating panels with improved UI - Split fixed left panel into floating panels (propertyPanel, creationPanel, control hints) - Convert rotation/flip buttons to SVG icons - Add checkbox row styling for fill/stroke/closed options - Add toggle button for control hints with eye/eye-off icons - Rename and restyle pixel on screen/canvas inputs - Update all UI text to proper capitalization - Add icons utility for SVG icon components - Update gitignore for build artifacts --- .gitignore | 2 + dist/index.html | 74 ++++--- dist/style.css | 246 +++++++++++++++++---- ts/App.ts | 30 ++- ts/editable/DrawableMultipleEditElement.ts | 41 ++-- ts/editable/DrawablePolylineEditElement.ts | 84 ++++--- ts/editable/DrawableTextEditElement.ts | 40 ++-- ts/elements/ColorAlphaElement.ts | 14 +- ts/elements/TitleElement.ts | 16 +- ts/util/Icons.ts | 37 ++++ 10 files changed, 416 insertions(+), 168 deletions(-) create mode 100644 ts/util/Icons.ts diff --git a/.gitignore b/.gitignore index 20ed833..251ccee 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /node_modules /.rpt2_cache /dist/app.js +/dist/*.map +/dist/app.js.LICENSE.txt diff --git a/dist/index.html b/dist/index.html index e364eba..1b13c70 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,5 +1,6 @@ + Chip Annotation Viewer @@ -9,43 +10,48 @@ -
-
-
-
-
- - - - -
-
- -
+
+
- -
- -

- -
- - -
- -
-
-
-
+
+
+
+ + +
+ +
+
+
+
+ +
+
+ +
-
-
- + diff --git a/dist/style.css b/dist/style.css index 7b17abd..039bc26 100644 --- a/dist/style.css +++ b/dist/style.css @@ -91,6 +91,8 @@ select:focus { border: 1px solid rgba(255, 255, 255, 0.1); display: flex; gap: 8px; + user-select: none; + -webkit-user-select: none; } .zoomButton { @@ -130,48 +132,114 @@ select:focus { font-size: 12px; } -#panel { - display: none; - float: left; - width: 240px; +#container { + position: relative; + width: 100%; height: 100%; - background: rgba(30, 41, 59, 0.95); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - flex-flow: column; - border-right: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); } -#panelScroll { - height: 100%; +#propertyPanel { + position: absolute; + top: 15px; + right: 15px; + z-index: 998; + background: rgba(30, 41, 59, 0.6); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + padding: 15px; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + min-width: 220px; + max-height: calc(100vh - 200px); overflow-y: auto; - flex: 1 1 auto; - padding: 10px; } -#panelScroll::-webkit-scrollbar { - width: 8px; +#propertyPanel::-webkit-scrollbar { + width: 6px; } -#panelScroll::-webkit-scrollbar-track { +#propertyPanel::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); - border-radius: 4px; + border-radius: 3px; } -#panelScroll::-webkit-scrollbar-thumb { +#propertyPanel::-webkit-scrollbar-thumb { background: var(--primary-color); - border-radius: 4px; + border-radius: 3px; } -#panelScroll::-webkit-scrollbar-thumb:hover { - background: var(--secondary-color); +#creationButtons { + display: flex; + gap: 4px; + flex-wrap: wrap; + margin: 10px 0; } -#container { +#hint { + position: absolute; + bottom: 70px; + left: 50%; + transform: translateX(-50%); + z-index: 997; + color: var(--text-secondary); + font-size: 13px; + padding: 8px 16px; + background: rgba(30, 41, 59, 0.6); + backdrop-filter: blur(10px); + border-radius: 8px; + max-width: 80%; + transition: opacity 0.3s ease; +} + +#hint.hidden { + opacity: 0; + pointer-events: none; +} + +.hintToggleButton { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); position: relative; - display: flex; - height: 100%; + overflow: hidden; + margin: 4px 4px; +} + +.hintToggleButton:hover { + transform: translateY(-1px); + box-shadow: 0 0 20px rgba(102, 126, 234, 0.4); +} + +.hintToggleButton:active { + transform: translateY(0); +} + +.hintToggleButton svg { + flex-shrink: 0; +} + +.hintToggleButton.hintHidden { + background: rgba(100, 116, 139, 0.6); +} + +.hintToggleButton.hintHidden:hover { + background: rgba(100, 116, 139, 0.8); + box-shadow: 0 0 15px rgba(100, 116, 139, 0.3); +} + +.hintToggleText { + font-size: 14px; } .configButton { @@ -217,6 +285,61 @@ select:focus { height: 300px; } +.iconButton { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + padding: 0; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin: 4px; +} + +.iconButton:hover { + transform: translateY(-1px); + box-shadow: 0 0 20px rgba(102, 126, 234, 0.4); +} + +.iconButton:active { + transform: translateY(0); +} + +.iconButton svg { + width: 20px; + height: 20px; +} + +.checkboxRow { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin: 8px 0; +} + +.checkboxLabel { + display: inline-flex; + align-items: center; + gap: 4px; + cursor: pointer; + font-size: 14px; + color: var(--text-secondary); + user-select: none; +} + +.checkboxLabel input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--primary-color); + cursor: pointer; +} + .configColorAlphaContainer { margin: 8px 0; padding: 12px; @@ -224,9 +347,34 @@ select:focus { border-radius: 8px; } -.colorAlphaContainerSplit { - width: 0; - display: inline-block; +.sizeInput { + display: flex; + align-items: center; + gap: 8px; + margin: 6px 0; +} + +.sizeInput label { + font-size: 13px; + color: var(--text-secondary); + white-space: nowrap; +} + +.sizeInput input[type="number"] { + width: 60px; + padding: 4px 8px; + background: var(--surface-color); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 13px; + text-align: center; +} + +.sizeInput input[type="number"]:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); } .configColorButton { @@ -303,16 +451,21 @@ input[type="number"]:focus { } #footer { - position: relative; + position: absolute; + bottom: 15px; + right: 15px; + z-index: 997; font-size: 14px; line-height: 24px; vertical-align: middle; display: flex; flex-flow: row; - justify-content: space-around; + /* gap: 8px; */ flex-wrap: wrap; - background: rgba(15, 23, 42, 0.8); - border-top: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(30, 41, 59, 0.6); + backdrop-filter: blur(10px); + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.1); } #footer a { @@ -321,7 +474,6 @@ input[type="number"]:focus { flex-flow: row; align-items: center; color: var(--text-secondary); - margin: 4px 0; padding: 8px 8px; border-radius: 20px; transition: all 0.2s ease; @@ -335,7 +487,7 @@ input[type="number"]:focus { } #footer span { - margin-left: 2px; + margin-left: 6px; cursor: pointer; line-height: 24px; vertical-align: middle; @@ -344,7 +496,6 @@ input[type="number"]:focus { .footerIcon { width: 20px; height: 20px; - margin-right: 8px; opacity: 0.8; } @@ -430,23 +581,34 @@ input[type="number"]:focus { } @media (max-width: 768px) { - #panel { - width: 200px; - } - .configButton { - padding: 10px 16px; - font-size: 13px; + padding: 8px 12px; + font-size: 12px; } #selectPanel, + #propertyPanel, #cameraPanel { - padding: 10px; + padding: 8px; border-radius: 8px; } + #propertyPanel { + min-width: 180px; + max-width: 200px; + } + .zoomButton { width: 36px; height: 36px; } + + #footer a span { + display: none; + } + + #hint { + font-size: 12px; + padding: 6px 12px; + } } diff --git a/ts/App.ts b/ts/App.ts index 0f95544..4db7993 100644 --- a/ts/App.ts +++ b/ts/App.ts @@ -24,12 +24,6 @@ let url_string = window.location.href; let url = new URL(url_string); let isReadOnly = !!url.searchParams.get("readonly"); -if (Ui.isMobile() || isReadOnly) { - document.getElementById("panel").style.display = "none"; -} else { - document.getElementById("panel").style.display = "flex"; -} - let canvas = new Canvas(document.getElementById("container"), 'canvas2d'); canvas.init(); @@ -79,6 +73,30 @@ class App { `, document.getElementById("selectPanel")); this.refresh(); + let hintElement = document.getElementById("hint"); + let hintToggle = document.getElementById("hintToggle") as HTMLButtonElement; + let hintIconEye = document.getElementById("hintIconEye") as HTMLElement; + let hintIconEyeOff = document.getElementById("hintIconEyeOff") as HTMLElement; + if (hintToggle) { + hintElement.classList.add("hidden"); + hintToggle.classList.add("hintHidden"); + if (hintIconEye) hintIconEye.style.display = "none"; + if (hintIconEyeOff) hintIconEyeOff.style.display = "block"; + hintToggle.onclick = () => { + hintElement.classList.toggle("hidden"); + hintToggle.classList.toggle("hintHidden"); + if (hintIconEye && hintIconEyeOff) { + if (hintElement.classList.contains("hidden")) { + hintIconEye.style.display = "none"; + hintIconEyeOff.style.display = "block"; + } else { + hintIconEye.style.display = "block"; + hintIconEyeOff.style.display = "none"; + } + } + }; + } + Selection.register(SelectType.POLYLINE, (item: DrawablePolyline) => { render(item.ui.render(canvas, this.map), document.getElementById("panelSelected")); canvas.enterEditors(EditorName.CAMERA_CONTROL, EditorName.SELECT, EditorName.POLYLINE_EDIT); diff --git a/ts/editable/DrawableMultipleEditElement.ts b/ts/editable/DrawableMultipleEditElement.ts index 282ab0e..cf9ccf5 100644 --- a/ts/editable/DrawableMultipleEditElement.ts +++ b/ts/editable/DrawableMultipleEditElement.ts @@ -1,11 +1,12 @@ -import {customElement, html, LitElement, property} from "lit-element"; -import {Canvas} from "../Canvas"; -import {AlphaEntry, ColorEntry} from "../util/Color"; +import { customElement, html, LitElement, property } from "lit-element"; +import { Canvas } from "../Canvas"; +import { AlphaEntry, ColorEntry } from "../util/Color"; import "../elements/ColorAlphaElement" -import {Selection} from "../layers/Selection"; -import {Drawable} from "../drawable/Drawable"; -import {EditableColor, EditableDeleteClone, EditableMove, editableMultiple} from "./Editable"; -import {TemplateResult} from "lit-html"; +import { Selection } from "../layers/Selection"; +import { Drawable } from "../drawable/Drawable"; +import { EditableColor, EditableDeleteClone, EditableMove, editableMultiple } from "./Editable"; +import { TemplateResult } from "lit-html"; +import { rotateCCWIcon, rotateCWIcon, flipXIcon, flipYIcon } from "../util/Icons"; @customElement('multipleedit-element') export class MultipleEdit extends LitElement { @@ -55,24 +56,24 @@ export class MultipleEdit extends LitElement { let editable = editableMultiple(drawables); return html` -
-
+
+
- - - - + + + + -
color
+
Color
{ - editable.setColorAlpha(color, undefined); - this.canvas.requestRender(); - }} + editable.setColorAlpha(color, undefined); + this.canvas.requestRender(); + }} .setAlpha=${(alpha: AlphaEntry) => { - editable.setColorAlpha(undefined, alpha); - this.canvas.requestRender(); - }} + editable.setColorAlpha(undefined, alpha); + this.canvas.requestRender(); + }} > `; } diff --git a/ts/editable/DrawablePolylineEditElement.ts b/ts/editable/DrawablePolylineEditElement.ts index 69b7060..cd8b217 100644 --- a/ts/editable/DrawablePolylineEditElement.ts +++ b/ts/editable/DrawablePolylineEditElement.ts @@ -1,10 +1,11 @@ -import {customElement, html, LitElement, property} from "lit-element"; -import {DrawablePolyline} from "./DrawablePolyline"; -import {Canvas} from "../Canvas"; -import {Map} from "../data/Map"; -import {AlphaEntry, ColorEntry} from "../util/Color"; +import { customElement, html, LitElement, property } from "lit-element"; +import { DrawablePolyline } from "./DrawablePolyline"; +import { Canvas } from "../Canvas"; +import { Map } from "../data/Map"; +import { AlphaEntry, ColorEntry } from "../util/Color"; import "../elements/ColorAlphaElement" -import {Selection, SelectType} from "../layers/Selection"; +import { Selection, SelectType } from "../layers/Selection"; +import { rotateCCWIcon, rotateCWIcon, flipXIcon, flipYIcon } from "../util/Icons"; @customElement('polylineedit-element') export class PolylineEdit extends LitElement { @@ -83,50 +84,65 @@ export class PolylineEdit extends LitElement { render() { return html` -
-
+
+
- + ${this.area}
- - - - + + + + -
+
+ + + +
- this.onStyleCheck(ev, {fill: (ev.target).checked})} .checked="${this.polyline.style.fill}" >fill
- this.onStyleCheck(ev, {stroke: (ev.target).checked})} .checked="${this.polyline.style.stroke}" >stroke
- this.onStyleCheck(ev, {closed: (ev.target).checked})} .checked="${this.polyline.style.closed}" >closed
- -
strokeColor
+
Stroke Color
{ - this.polyline.style.setStrokeColor(color, undefined); - this.canvas.requestRender(); - }} + this.polyline.style.setStrokeColor(color, undefined); + this.canvas.requestRender(); + }} .setAlpha=${(alpha: AlphaEntry) => { - this.polyline.style.setStrokeColor(undefined, alpha); - this.canvas.requestRender(); - }} + this.polyline.style.setStrokeColor(undefined, alpha); + this.canvas.requestRender(); + }} > -
fillColor
+
Fill Color
{ - this.polyline.style.setFillColor(color, undefined); - this.canvas.requestRender(); - }} + this.polyline.style.setFillColor(color, undefined); + this.canvas.requestRender(); + }} .setAlpha=${(alpha: AlphaEntry) => { - this.polyline.style.setFillColor(undefined, alpha); - this.canvas.requestRender(); - }} + this.polyline.style.setFillColor(undefined, alpha); + this.canvas.requestRender(); + }} > - this.onSizeInput(ev, {screen: (ev.target).value})}>pixel onScreen
- this.onSizeInput(ev, {canvas: (ev.target).value})}>pixel onCanvas
+
+ this.onSizeInput(ev, { screen: (ev.target).value })}> + +
+
+ this.onSizeInput(ev, { canvas: (ev.target).value })}> + +
`; } diff --git a/ts/editable/DrawableTextEditElement.ts b/ts/editable/DrawableTextEditElement.ts index e421ada..1b9d0be 100644 --- a/ts/editable/DrawableTextEditElement.ts +++ b/ts/editable/DrawableTextEditElement.ts @@ -1,9 +1,9 @@ -import {customElement, html, LitElement, property} from "lit-element"; -import {DrawableText} from "./DrawableText"; -import {Canvas} from "../Canvas"; -import {AlphaEntry, ColorEntry} from "../util/Color"; +import { customElement, html, LitElement, property } from "lit-element"; +import { DrawableText } from "./DrawableText"; +import { Canvas } from "../Canvas"; +import { AlphaEntry, ColorEntry } from "../util/Color"; import "../elements/ColorAlphaElement" -import {Selection, SelectType} from "../layers/Selection"; +import { Selection, SelectType } from "../layers/Selection"; @customElement('textedit-element') export class TextEdit extends LitElement { @@ -39,26 +39,32 @@ export class TextEdit extends LitElement { render() { return html` -
-
+
+
- text
+ Text
this.editText((ev.target).value)}>
-
color
+
Color
{ - this.text.setColorAlpha(color, undefined); - this.canvas.requestRender(); - }} + this.text.setColorAlpha(color, undefined); + this.canvas.requestRender(); + }} .setAlpha=${(alpha: AlphaEntry) => { - this.text.setColorAlpha(undefined, alpha); - this.canvas.requestRender(); - }} + this.text.setColorAlpha(undefined, alpha); + this.canvas.requestRender(); + }} > - this.onSizeInput(ev, {screen: (ev.target).value})}>pixel onScreen
- this.onSizeInput(ev, {canvas: (ev.target).value})}>pixel onCanvas
+
+ this.onSizeInput(ev, { screen: (ev.target).value })}> + +
+
+ this.onSizeInput(ev, { canvas: (ev.target).value })}> + +
`; } diff --git a/ts/elements/ColorAlphaElement.ts b/ts/elements/ColorAlphaElement.ts index 2baaa88..83ccb3e 100644 --- a/ts/elements/ColorAlphaElement.ts +++ b/ts/elements/ColorAlphaElement.ts @@ -1,5 +1,5 @@ -import {customElement, html, LitElement, property} from "lit-element"; -import {AlphaEntry, ColorEntry} from "../util/Color"; +import { customElement, html, LitElement, property } from "lit-element"; +import { AlphaEntry, ColorEntry } from "../util/Color"; @customElement('coloralpha-element') export class ColorAlphaElement extends LitElement { @@ -12,11 +12,11 @@ export class ColorAlphaElement extends LitElement { return html`
${ColorEntry.list.map(color => html``)} - +
${AlphaEntry.list.map(alpha => { - let color = 255 * (1 - alpha.value); - return html`` - })} + let color = 255 * (1 - alpha.value); + return html`` + })}
`; } @@ -25,4 +25,4 @@ export class ColorAlphaElement extends LitElement { return this; } -} \ No newline at end of file +} diff --git a/ts/elements/TitleElement.ts b/ts/elements/TitleElement.ts index 8806a15..c1d5220 100644 --- a/ts/elements/TitleElement.ts +++ b/ts/elements/TitleElement.ts @@ -1,8 +1,8 @@ -import {customElement, html, LitElement, property} from "lit-element"; -import {Annotation} from "../data/Data"; -import {Map} from "../data/Map"; -import {Canvas} from "../Canvas"; -import {Github} from "../util/GithubUtil"; +import { customElement, html, LitElement, property } from "lit-element"; +import { Annotation } from "../data/Data"; +import { Map } from "../data/Map"; +import { Canvas } from "../Canvas"; +import { Github } from "../util/GithubUtil"; @customElement('title-element') export class TitleElement extends LitElement { @@ -20,7 +20,7 @@ export class TitleElement extends LitElement { let data = this.canvas.save(); data.title = (document.getElementById("inputTitle") as HTMLInputElement).value; if (data.title == null || data.title == "") { - data.title = "untitled"; + data.title = "Untitled"; } let dataString = JSON.stringify(data); @@ -71,10 +71,10 @@ export class TitleElement extends LitElement { } return html` - +
- + ? `; } diff --git a/ts/util/Icons.ts b/ts/util/Icons.ts new file mode 100644 index 0000000..d17ed8a --- /dev/null +++ b/ts/util/Icons.ts @@ -0,0 +1,37 @@ +import { html } from "lit-html"; + +export const rotateCCWIcon = html` + + + + +`; + +export const rotateCWIcon = html` + + + + +`; + +export const flipXIcon = html` + + + + + + + + +`; + +export const flipYIcon = html` + + + + + + + + +`; From 32462a239ac8bd770be62ec00cf056a357f28722 Mon Sep 17 00:00:00 2001 From: misdake Date: Wed, 25 Mar 2026 20:55:57 +0800 Subject: [PATCH 03/12] Add color selection state to ColorAlphaElement --- ts/editable/DrawableMultipleEditElement.ts | 2 +- ts/editable/DrawablePolylineEditElement.ts | 4 ++++ ts/editable/DrawableTextEditElement.ts | 4 +++- ts/elements/ColorAlphaElement.ts | 19 +++++++++++++++++-- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/ts/editable/DrawableMultipleEditElement.ts b/ts/editable/DrawableMultipleEditElement.ts index cf9ccf5..23edf2e 100644 --- a/ts/editable/DrawableMultipleEditElement.ts +++ b/ts/editable/DrawableMultipleEditElement.ts @@ -57,7 +57,7 @@ export class MultipleEdit extends LitElement { return html`
-
+
diff --git a/ts/editable/DrawablePolylineEditElement.ts b/ts/editable/DrawablePolylineEditElement.ts index cd8b217..fbbc2ff 100644 --- a/ts/editable/DrawablePolylineEditElement.ts +++ b/ts/editable/DrawablePolylineEditElement.ts @@ -114,6 +114,8 @@ export class PolylineEdit extends LitElement {
Stroke Color
{ this.polyline.style.setStrokeColor(color, undefined); this.canvas.requestRender(); @@ -125,6 +127,8 @@ export class PolylineEdit extends LitElement { >
Fill Color
{ this.polyline.style.setFillColor(color, undefined); this.canvas.requestRender(); diff --git a/ts/editable/DrawableTextEditElement.ts b/ts/editable/DrawableTextEditElement.ts index 1b9d0be..8b00539 100644 --- a/ts/editable/DrawableTextEditElement.ts +++ b/ts/editable/DrawableTextEditElement.ts @@ -40,13 +40,15 @@ export class TextEdit extends LitElement { render() { return html`
-
+
Text
this.editText((ev.target).value)}>
Color
{ this.text.setColorAlpha(color, undefined); this.canvas.requestRender(); diff --git a/ts/elements/ColorAlphaElement.ts b/ts/elements/ColorAlphaElement.ts index 83ccb3e..88d6521 100644 --- a/ts/elements/ColorAlphaElement.ts +++ b/ts/elements/ColorAlphaElement.ts @@ -6,16 +6,31 @@ export class ColorAlphaElement extends LitElement { @property() private setColor: (color: ColorEntry) => void; + @property() private setAlpha: (alpha: AlphaEntry) => void; + @property() + private currentColor: ColorEntry; + @property() + private currentAlpha: AlphaEntry; render() { return html` +
- ${ColorEntry.list.map(color => html``)} + ${ColorEntry.list.map(color => html``)}
${AlphaEntry.list.map(alpha => { let color = 255 * (1 - alpha.value); - return html`` + return html`` })}
`; From 9e29d5a271e2a4b956a244ef8cfae0e569737c26 Mon Sep 17 00:00:00 2001 From: misdake Date: Thu, 26 Mar 2026 01:00:03 +0800 Subject: [PATCH 04/12] implement left-button pick aabb with realtime preview, replacing ctrl+left selection. --- ts/editors/EditorSelect.ts | 312 ++++++++++++++++++++++++++----------- 1 file changed, 224 insertions(+), 88 deletions(-) diff --git a/ts/editors/EditorSelect.ts b/ts/editors/EditorSelect.ts index c742137..57c83dd 100644 --- a/ts/editors/EditorSelect.ts +++ b/ts/editors/EditorSelect.ts @@ -1,27 +1,34 @@ -import {Editor, Usage} from "./Editor"; -import {MouseIn, MouseListener} from "../MouseListener"; -import {EditorName} from "./Editors"; -import {Canvas} from "../Canvas"; -import {DrawablePolyline} from "../editable/DrawablePolyline"; -import {Selection, SelectType} from "../layers/Selection"; -import {Env} from "../Env"; -import {Renderer} from "../Renderer"; -import {DrawableText} from "../editable/DrawableText"; -import {Drawable} from "../drawable/Drawable"; -import {EditablePick} from "../editable/Editable"; -import {Camera} from "../Camera"; +import { Editor, Usage } from "./Editor"; +import { MouseIn, MouseListener } from "../MouseListener"; +import { EditorName } from "./Editors"; +import { Canvas } from "../Canvas"; +import { DrawablePolyline } from "../editable/DrawablePolyline"; +import { Selection, SelectType } from "../layers/Selection"; +import { Env } from "../Env"; +import { Renderer } from "../Renderer"; +import { DrawableText } from "../editable/DrawableText"; +import { Drawable } from "../drawable/Drawable"; +import { EditablePick } from "../editable/Editable"; +import { Camera } from "../Camera"; export class EditorSelect extends Editor { + private dragging = false; + private dragStartX = 0; + private dragStartY = 0; + private dragCurrentX = 0; + private dragCurrentY = 0; + private previewSelection: (Drawable & EditablePick)[] = []; + constructor(canvas: Canvas) { super(EditorName.SELECT, canvas); } usages(): Usage[] { return [ - Editor.usage("left click polygon/text to select"), + Editor.usage("left click to select"), Editor.usage("hold ctrl and left click to select another"), - Editor.usage("hold ctrl and drag to select along"), + Editor.usage("left click and drag to select multiple"), ]; } @@ -29,102 +36,164 @@ export class EditorSelect extends Editor { let self = this; this._mouseListener = new class extends MouseListener { - private moved = false; onmousedown(event: MouseIn): boolean { - this.moved = false; + if (event.button == 0) { + self.dragging = false; + let canvasXY = self.camera.screenXyToCanvas(event.offsetX, event.offsetY); + self.dragStartX = canvasXY.x; + self.dragStartY = canvasXY.y; + self.dragCurrentX = canvasXY.x; + self.dragCurrentY = canvasXY.y; + return true; + } return false; } onmouseup(event: MouseIn): boolean { - if (event.button == 0 && !this.moved) { + if (event.button == 0) { let canvasXY = self.camera.screenXyToCanvas(event.offsetX, event.offsetY); let x = canvasXY.x, y = canvasXY.y; - let {item, type} = self.pickAny(x, y, env); - if (item) { - if (!event.ctrlKey) { - Selection.select(type, item); - return true; - } else { - let current = Selection.getSelected(); - let currentType = current.type; + if (self.dragging) { + let selected = self.previewSelection; + + if (selected.length > 0) { + if (!event.ctrlKey) { + if (selected.length === 1) { + Selection.select(selected[0].pickType, selected[0]); + } else { + Selection.select(SelectType.MULTIPLE, selected); + } + } else { + let current = Selection.getSelected(); + let currentType = current.type; + if (!currentType) { + if (selected.length === 1) { + Selection.select(selected[0].pickType, selected[0]); + } else { + Selection.select(SelectType.MULTIPLE, selected); + } + } else if (currentType === SelectType.MULTIPLE) { + let array = <(Drawable & EditablePick)[]>current.item; + for (let item of selected) { + if (array.indexOf(item) < 0) { + array.push(item); + } + } + Selection.select(SelectType.MULTIPLE, array); + } else { + let newArray: (Drawable & EditablePick)[] = [current.item]; + for (let item of selected) { + if (newArray.indexOf(item) < 0) { + newArray.push(item); + } + } + Selection.select(SelectType.MULTIPLE, newArray); + } + } + } - if (!currentType) { //nothing selected + self.dragging = false; + self.previewSelection = []; + return true; + } else { + let { item, type } = self.pickAny(x, y, env); + if (item) { + if (!event.ctrlKey) { Selection.select(type, item); return true; + } else { + let current = Selection.getSelected(); + let currentType = current.type; - } else if (currentType === SelectType.MULTIPLE) { //multiple selected - let array = <(Drawable & EditablePick)[]>current.item; - let index = array.indexOf(item); - if (index >= 0) { //deselecting existing - array.splice(index, 1); //remove this - if (array.length === 0) { //deselected everything - Selection.deselectAny(); - return true; - } else if (array.length === 1) { //only one left - Selection.select(array[0].pickType, array[0]); - return true; - } else { //update current array + if (!currentType) { + Selection.select(type, item); + return true; + + } else if (currentType === SelectType.MULTIPLE) { + let array = <(Drawable & EditablePick)[]>current.item; + let index = array.indexOf(item); + if (index >= 0) { + array.splice(index, 1); + if (array.length === 0) { + Selection.deselectAny(); + return true; + } else if (array.length === 1) { + Selection.select(array[0].pickType, array[0]); + return true; + } else { + Selection.select(SelectType.MULTIPLE, array); + return true; + } + } else { + array.push(item); Selection.select(SelectType.MULTIPLE, array); return true; } - } else { //select new - array.push(item); - Selection.select(SelectType.MULTIPLE, array); - return true; - } - } else { - if (current.item !== item) { //selected a second - Selection.select(SelectType.MULTIPLE, [current.item, item]); - return true; - } else { //deselected that - Selection.deselectAny(); - return true; + } else { + if (current.item !== item) { + Selection.select(SelectType.MULTIPLE, [current.item, item]); + return true; + } else { + Selection.deselectAny(); + return true; + } } } } - } - Selection.deselectAny(); - return false; - } else { - return false; + Selection.deselectAny(); + return false; + } } + return false; } onmousemove(event: MouseIn): boolean { if (event.buttons & 1) { - this.moved = true; - - if (event.ctrlKey) { - let canvasXY = self.camera.screenXyToCanvas(event.offsetX, event.offsetY); - let x = canvasXY.x, y = canvasXY.y; - - let current = Selection.getSelected(); - let currentType = current.type; - - let exclude: (Drawable & EditablePick)[] = []; - switch (currentType) { - case SelectType.POLYLINE: - case SelectType.TEXT: - exclude = [<(Drawable & EditablePick)>current.item]; - break; - case SelectType.MULTIPLE: - exclude = <(Drawable & EditablePick)[]>current.item; - break; - } + let canvasXY = self.camera.screenXyToCanvas(event.offsetX, event.offsetY); + let x = canvasXY.x, y = canvasXY.y; - let picked = self.pickAll(x, y, env, undefined, exclude); + if (Math.abs(x - self.dragStartX) > 3 || Math.abs(y - self.dragStartY) > 3) { + self.dragging = true; + } - if (picked.length) { - if(exclude.length === 0 && picked.length === 1) { - let item = picked[0]; - Selection.select(item.pickType, item); - } else { - exclude.push(...picked); //try to change the original array for MULTIPLE, so EditorMultiple will continue to work. - Selection.select(SelectType.MULTIPLE, exclude); + if (self.dragging) { + self.dragCurrentX = x; + self.dragCurrentY = y; + + let startX = self.dragStartX; + let startY = self.dragStartY; + let endX = self.dragCurrentX; + let endY = self.dragCurrentY; + + let minX = Math.min(startX, endX); + let maxX = Math.max(startX, endX); + let minY = Math.min(startY, endY); + let maxY = Math.max(startY, endY); + + self.previewSelection = []; + + for (let text of env.texts) { + let aabb = self.getAABB(text); + if (aabb) { + let partiallyContained = !(aabb.x2 < minX || aabb.x1 > maxX || aabb.y2 < minY || aabb.y1 > maxY); + if (partiallyContained) { + self.previewSelection.push(text); + } } } + for (let polyline of env.polylines) { + let aabb = self.getAABB(polyline); + if (aabb) { + let partiallyContained = !(aabb.x2 < minX || aabb.x1 > maxX || aabb.y2 < minY || aabb.y1 > maxY); + if (partiallyContained) { + self.previewSelection.push(polyline); + } + } + } + + self.canvas.requestRender(); return true; } } @@ -134,8 +203,8 @@ export class EditorSelect extends Editor { if (event.button == 0) { let canvasXY = self.camera.screenXyToCanvas(event.offsetX, event.offsetY); let x = canvasXY.x, y = canvasXY.y; - let {item, type} = self.pickAny(x, y, env); - if (type === SelectType.TEXT) { //double click a text with link to open link + let { item, type } = self.pickAny(x, y, env); + if (type === SelectType.TEXT) { let text = item; if (text.link) { window.open(text.link, "_blank"); @@ -226,23 +295,42 @@ export class EditorSelect extends Editor { //text first let text = EditorSelect.pickText(x, y, env.camera, texts); if (text) { - return {item: text, type: SelectType.TEXT}; + return { item: text, type: SelectType.TEXT }; } //polyline next let polyline = EditorSelect.pickPolyline(x, y, env.camera, polylines); if (polyline) { - return {item: polyline, type: SelectType.POLYLINE}; + return { item: polyline, type: SelectType.POLYLINE }; } - return {item: undefined, type: undefined}; + return { item: undefined, type: undefined }; }; + public getAABB(item: Drawable & EditablePick): { x1: number, y1: number, x2: number, y2: number } { + switch (item.pickType) { + case SelectType.TEXT: + let text = item; + return text.validateCanvasAABB(this.camera, null); + case SelectType.POLYLINE: + let polyline = item; + let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity; + polyline.editor.forEachPoint((x: number, y: number) => { + if (x < x1) x1 = x; + if (y < y1) y1 = y; + if (x > x2) x2 = x; + if (y > y2) y2 = y; + }); + return { x1, y1, x2, y2 }; + } + return null; + } + //render render(env: Env): void { - let {item: item, type: type} = Selection.getSelected(); + let { item: item, type: type } = Selection.getSelected(); switch (type) { case SelectType.POLYLINE: case SelectType.POLYLINE_CREATE: @@ -264,6 +352,30 @@ export class EditorSelect extends Editor { } break; } + + if (this.dragging) { + let p1 = this.camera.canvasToScreen(this.dragStartX, this.dragStartY); + let p2 = this.camera.canvasToScreen(this.dragCurrentX, this.dragCurrentY); + let x1 = Math.min(p1.x, p2.x); + let y1 = Math.min(p1.y, p2.y); + let x2 = Math.max(p1.x, p2.x); + let y2 = Math.max(p1.y, p2.y); + env.renderer.setColor("rgba(100, 149, 237, 0.3)"); + env.renderer.drawRect(x1, y1, x2, y2, true, false); + env.renderer.setColor("rgba(100, 149, 237, 1)"); + env.renderer.drawRect(x1, y1, x2, y2, false, true); + + for (let drawable of this.previewSelection) { + switch (drawable.pickType) { + case SelectType.POLYLINE: + this.drawPreviewPolyline(drawable, env.renderer); + break; + case SelectType.TEXT: + this.drawPreviewText(drawable, env.renderer); + break; + } + } + } } private drawSelectedPolyline(polyline: DrawablePolyline, renderer: Renderer) { @@ -290,4 +402,28 @@ export class EditorSelect extends Editor { ); } + private drawPreviewPolyline(polyline: DrawablePolyline, renderer: Renderer) { + let drawPointCircle = (x: number, y: number, renderer: Renderer) => { + let position = this.camera.canvasToScreen(x, y); + renderer.setColor("rgba(255,255,255,1)"); + renderer.drawCircle(position.x, position.y, 5, false, true, 1); + renderer.setColor("rgba(0,0,0,0.5)"); + renderer.drawCircle(position.x, position.y, 4, true, false); + }; + polyline.editor.forEachPoint((x: number, y: number) => { + drawPointCircle(x, y, renderer); + }); + } + + private drawPreviewText(text: DrawableText, renderer: Renderer) { + renderer.setColor(text.colorString); + let aabb = text.validateCanvasAABB(this.camera, renderer); + let p1 = this.camera.canvasToScreen(aabb.x1, aabb.y1); + let p2 = this.camera.canvasToScreen(aabb.x2, aabb.y2); + renderer.drawRect( + p1.x - 5, p1.y - 5, p2.x + 5, p2.y + 5, + false, true, 2 + ); + } + } From 1e70106836e638b84038c977aab87e17e1d5734c Mon Sep 17 00:00:00 2001 From: misdake Date: Thu, 26 Mar 2026 01:20:51 +0800 Subject: [PATCH 05/12] refine source link. refine chip selection ui. --- dist/style.css | 15 ++++++++++----- ts/elements/SelectElement.ts | 26 +++++++++++++------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/dist/style.css b/dist/style.css index 039bc26..2367bb3 100644 --- a/dist/style.css +++ b/dist/style.css @@ -27,8 +27,8 @@ html { select { min-width: 150px; - height: 36px; - padding: 8px 12px; + height: 24px; + padding: 0; background: var(--surface-color); color: var(--text-primary); border: 1px solid var(--border-color); @@ -126,8 +126,13 @@ select:focus { align-items: center; } -#imageSource { - display: inline-block; +.imageSource { + margin-left: 4px; + color: var(--text-secondary); + font-size: 12px; +} + +.imageSourceLabel { color: var(--text-secondary); font-size: 12px; } @@ -611,4 +616,4 @@ input[type="number"]:focus { font-size: 12px; padding: 6px 12px; } -} +} \ No newline at end of file diff --git a/ts/elements/SelectElement.ts b/ts/elements/SelectElement.ts index 64d9ff8..ae57fe5 100644 --- a/ts/elements/SelectElement.ts +++ b/ts/elements/SelectElement.ts @@ -1,8 +1,8 @@ -import {customElement, html, LitElement, property, TemplateResult} from "lit-element"; -import {Chip, Map} from "../data/Map"; -import {NetUtil} from "../util/NetUtil"; -import {Annotation, Data} from "../data/Data"; -import {Github} from "../util/GithubUtil"; +import { customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import { Chip, Map } from "../data/Map"; +import { NetUtil } from "../util/NetUtil"; +import { Annotation, Data } from "../data/Data"; +import { Github } from "../util/GithubUtil"; @customElement('select-element') export class SelectElement extends LitElement { @@ -42,7 +42,7 @@ export class SelectElement extends LitElement { @property() annotation_current: Annotation; - private static dummyAnnotation: Annotation = {id: 0, user: "", content: {title: "", polylines: [], texts: []}}; + private static dummyAnnotation: Annotation = { id: 0, user: "", content: { title: "", polylines: [], texts: [] } }; //↑↑↑↑↑ annotation selection box ↑↑↑↑↑ @@ -55,7 +55,7 @@ export class SelectElement extends LitElement { protected firstUpdated(): void { let url_string = window.location.href; let url = new URL(url_string); - this.chip_name_toload = (url.searchParams.get("map") && decodeURIComponent(url.searchParams.get("map")) )|| "Fiji"; + this.chip_name_toload = (url.searchParams.get("map") && decodeURIComponent(url.searchParams.get("map"))) || "Fiji"; this.annotation_id_toload = parseInt(url.searchParams.get("commentId") || "0"); this.refreshChipList(); @@ -97,7 +97,7 @@ export class SelectElement extends LitElement { } private refreshChipList() { SelectElement.fetchChipList().then(chips => { - let {html, array, current} = SelectElement.showChipList(chips, this.chip_name_toload); + let { html, array, current } = SelectElement.showChipList(chips, this.chip_name_toload); this.chip_current = current; this.chiplist_html = html; this.chiplist_array = array; @@ -114,7 +114,7 @@ export class SelectElement extends LitElement { this.annotationlist_html = []; this.annotationlist_array = []; SelectElement.fetchAnnotationList(this.map_current).then(annotations => { - let {html, array, current} = SelectElement.showAnnotationList(annotations, this.annotation_id_toload); + let { html, array, current } = SelectElement.showAnnotationList(annotations, this.annotation_id_toload); this.annotation_current = current; this.annotationlist_html = html; this.annotationlist_array = array; @@ -187,7 +187,7 @@ export class SelectElement extends LitElement { last_Family = curr_Family; } - return {html: selections, array: selection_chip, current: current}; + return { html: selections, array: selection_chip, current: current }; } private static fetchMap(chip: Chip): Promise { @@ -211,7 +211,7 @@ export class SelectElement extends LitElement { if (data.title == null || data.title == "") { data.title = "untitled"; } - result.push({id: comment.id, user: comment.user.login, content: data}); + result.push({ id: comment.id, user: comment.user.login, content: data }); } } catch (_e) { // if anything goes wrong, ignore it, it's not a valid annotation @@ -249,11 +249,11 @@ export class SelectElement extends LitElement { } } - return {html: options, array: array, current: current} + return { html: options, array: array, current: current } } render() { - let source = this.map_current ? html`${this.map_current.source}` : html``; + let source = this.map_current ? html`` : html``; return html`
From 76dfc46abf2683624ba38ac111494467ace6d14d Mon Sep 17 00:00:00 2001 From: misdake Date: Thu, 26 Mar 2026 02:32:45 +0800 Subject: [PATCH 06/12] refine title css. tweak select rect opacity. --- dist/style.css | 33 ++++++++++++++++++++++++++++++++- ts/editors/EditorSelect.ts | 2 +- ts/elements/TitleElement.ts | 7 ++++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/dist/style.css b/dist/style.css index 2367bb3..328c3ab 100644 --- a/dist/style.css +++ b/dist/style.css @@ -430,11 +430,42 @@ select:focus { } .configText { - margin: 8px 0; + margin: 4px; font-size: 14px; color: var(--text-secondary); } +.titleInput { + display: flex; + align-items: center; + gap: 4px; +} + +.titleInput label { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; + white-space: nowrap; +} + +.titleInput input[type="text"] { + flex: 1; + padding: 6px 10px; + background: var(--surface-color); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 14px; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.titleInput input[type="text"]:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); +} + input[type="text"], input[type="number"] { width: 100%; diff --git a/ts/editors/EditorSelect.ts b/ts/editors/EditorSelect.ts index 57c83dd..9aa33ae 100644 --- a/ts/editors/EditorSelect.ts +++ b/ts/editors/EditorSelect.ts @@ -360,7 +360,7 @@ export class EditorSelect extends Editor { let y1 = Math.min(p1.y, p2.y); let x2 = Math.max(p1.x, p2.x); let y2 = Math.max(p1.y, p2.y); - env.renderer.setColor("rgba(100, 149, 237, 0.3)"); + env.renderer.setColor("rgba(100, 149, 237, 0.2)"); env.renderer.drawRect(x1, y1, x2, y2, true, false); env.renderer.setColor("rgba(100, 149, 237, 1)"); env.renderer.drawRect(x1, y1, x2, y2, false, true); diff --git a/ts/elements/TitleElement.ts b/ts/elements/TitleElement.ts index c1d5220..ef2a225 100644 --- a/ts/elements/TitleElement.ts +++ b/ts/elements/TitleElement.ts @@ -71,9 +71,10 @@ export class TitleElement extends LitElement { } return html` - - -
+
+ + +
? `; From ac549176998fdf627282466a5eb6ab850222758a Mon Sep 17 00:00:00 2001 From: misdake Date: Thu, 26 Mar 2026 16:24:39 +0800 Subject: [PATCH 07/12] refine text. --- ts/editable/DrawablePolylineEditElement.ts | 2 +- ts/editable/DrawableTextEditElement.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/editable/DrawablePolylineEditElement.ts b/ts/editable/DrawablePolylineEditElement.ts index fbbc2ff..e6588b0 100644 --- a/ts/editable/DrawablePolylineEditElement.ts +++ b/ts/editable/DrawablePolylineEditElement.ts @@ -32,7 +32,7 @@ export class PolylineEdit extends LitElement { calcArea() { let width = this.map.widthMillimeter; let height = this.map.heightMillimeter; - let unit = this.polyline.style.fill ? "mm^2" : "mm"; + let unit = this.polyline.style.fill ? "mm²" : "mm"; if (!(this.map.widthMillimeter > 0 && this.map.heightMillimeter > 0)) { width = this.map.width; height = this.map.height; diff --git a/ts/editable/DrawableTextEditElement.ts b/ts/editable/DrawableTextEditElement.ts index 8b00539..3e2aa0b 100644 --- a/ts/editable/DrawableTextEditElement.ts +++ b/ts/editable/DrawableTextEditElement.ts @@ -45,7 +45,7 @@ export class TextEdit extends LitElement { Text
this.editText((ev.target).value)}>
-
Color
+
Text Color
Date: Sat, 28 Mar 2026 00:35:47 +0800 Subject: [PATCH 08/12] Fix selection box not disappearing when dragging selection with no results --- ts/editors/EditorSelect.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ts/editors/EditorSelect.ts b/ts/editors/EditorSelect.ts index 9aa33ae..6ef0428 100644 --- a/ts/editors/EditorSelect.ts +++ b/ts/editors/EditorSelect.ts @@ -90,10 +90,15 @@ export class EditorSelect extends Editor { Selection.select(SelectType.MULTIPLE, newArray); } } + } else { + if (!event.ctrlKey) { + Selection.deselectAny(); + } } self.dragging = false; self.previewSelection = []; + self.canvas.requestRender(); return true; } else { let { item, type } = self.pickAny(x, y, env); From 1c32ef93f94486b543a69be0e2079d61d4128167 Mon Sep 17 00:00:00 2001 From: misdake Date: Sat, 28 Mar 2026 09:53:15 +0800 Subject: [PATCH 09/12] Add tri-state checkbox component for fill/stroke/closed options --- dist/style.css | 2 +- ts/editable/DrawablePolylineEditElement.ts | 28 ++++--- ts/elements/TriStateCheckboxElement.ts | 95 ++++++++++++++++++++++ 3 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 ts/elements/TriStateCheckboxElement.ts diff --git a/dist/style.css b/dist/style.css index 328c3ab..ec7c0d9 100644 --- a/dist/style.css +++ b/dist/style.css @@ -156,7 +156,7 @@ select:focus { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.1); min-width: 220px; - max-height: calc(100vh - 200px); + max-height: calc(100vh - 100px); overflow-y: auto; } diff --git a/ts/editable/DrawablePolylineEditElement.ts b/ts/editable/DrawablePolylineEditElement.ts index e6588b0..2b35b9d 100644 --- a/ts/editable/DrawablePolylineEditElement.ts +++ b/ts/editable/DrawablePolylineEditElement.ts @@ -4,6 +4,7 @@ import { Canvas } from "../Canvas"; import { Map } from "../data/Map"; import { AlphaEntry, ColorEntry } from "../util/Color"; import "../elements/ColorAlphaElement" +import "../elements/TriStateCheckboxElement" import { Selection, SelectType } from "../layers/Selection"; import { rotateCCWIcon, rotateCWIcon, flipXIcon, flipYIcon } from "../util/Icons"; @@ -98,18 +99,21 @@ export class PolylineEdit extends LitElement {
- - - + + +
Stroke Color
diff --git a/ts/elements/TriStateCheckboxElement.ts b/ts/elements/TriStateCheckboxElement.ts new file mode 100644 index 0000000..5702d9e --- /dev/null +++ b/ts/elements/TriStateCheckboxElement.ts @@ -0,0 +1,95 @@ +import { css, customElement, html, LitElement, property } from 'lit-element'; + +@customElement('tristate-checkbox') +export class TriStateCheckboxElement extends LitElement { + + @property() + state: 'none' | 'some' | 'all' = 'none'; + + @property() + label: string = ''; + + @property() + onChange: (state: 'none' | 'all') => void; + + static override styles = css` + :host { + display: inline; + cursor: pointer; + user-select: none; + // line-height: 24px; + // height: 24px; + vertical-align: middle; + } + + .tsc-box { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid #666; + border-radius: 3px; + background: #fff; + transition: all 0.2s; + vertical-align: middle; + text-align: center; + } + + :host(:hover) .tsc-box { + border-color: #333; + } + + .tsc-box svg { + width: 14px; + height: 14px; + vertical-align: top; + } + + .tsc-box.tsc-checked { + background: #4a90d9; + border-color: #4a90d9; + } + + .tsc-label { + display: inline; + white-space: nowrap; + vertical-align: middle; + } + `; + + private toggle() { + if (this.state === 'all') { + this.state = 'none'; + } else { + this.state = 'all'; + } + if (this.onChange) this.onChange(this.state); + } + + private getSvg() { + if (this.state === 'none') { + return html``; + } else if (this.state === 'some') { + return html` + + + + `; + } else { + return html` + + + + `; + } + } + + render() { + return html` + this.toggle()}> + ${this.getSvg()} + + ${this.label ? html` this.toggle()}>${this.label}` : ''} + `; + } + +} From 98ec29068455725979f7294818118a3f33467984 Mon Sep 17 00:00:00 2001 From: misdake Date: Sat, 28 Mar 2026 12:01:52 +0800 Subject: [PATCH 10/12] Refactor polyline and text edit elements to support array input with unified state management --- ts/editable/DrawablePolyline.ts | 40 +++--- ts/editable/DrawablePolylineEditElement.ts | 154 +++++++++++++++------ ts/editable/DrawableText.ts | 30 ++-- ts/editable/DrawableTextEditElement.ts | 81 ++++++++--- ts/elements/ColorAlphaElement.ts | 31 ++++- ts/elements/NumberInputElement.ts | 97 +++++++++++++ ts/elements/TextInputElement.ts | 73 ++++++++++ ts/util/MultiSelect.ts | 25 ++++ 8 files changed, 434 insertions(+), 97 deletions(-) create mode 100644 ts/elements/NumberInputElement.ts create mode 100644 ts/elements/TextInputElement.ts create mode 100644 ts/util/MultiSelect.ts diff --git a/ts/editable/DrawablePolyline.ts b/ts/editable/DrawablePolyline.ts index 7ee3e26..070591e 100644 --- a/ts/editable/DrawablePolyline.ts +++ b/ts/editable/DrawablePolyline.ts @@ -1,17 +1,17 @@ -import {Drawable} from "../drawable/Drawable"; -import {Canvas} from "../Canvas"; -import {Renderer} from "../Renderer"; -import {Camera} from "../Camera"; -import {Size} from "../util/Size"; -import {AlphaEntry, ColorEntry, combineColorAlpha} from "../util/Color"; -import {AABB} from "../util/AABB"; -import {html, TemplateResult} from "lit-html"; -import {Map} from "../data/Map"; -import {Primitive, PrimitivePack} from "./Primitive"; -import {EditableColor, EditableDeleteClone, EditableMove, EditablePick} from "./Editable"; -import {LayerPolylineView} from "../layers/LayerPolylineView"; -import {LayerName} from "../layers/Layers"; -import {Selection, SelectType} from "../layers/Selection"; +import { Drawable } from "../drawable/Drawable"; +import { Canvas } from "../Canvas"; +import { Renderer } from "../Renderer"; +import { Camera } from "../Camera"; +import { Size } from "../util/Size"; +import { AlphaEntry, ColorEntry, combineColorAlpha } from "../util/Color"; +import { AABB } from "../util/AABB"; +import { html, TemplateResult } from "lit-html"; +import { Map } from "../data/Map"; +import { Primitive, PrimitivePack } from "./Primitive"; +import { EditableColor, EditableDeleteClone, EditableMove, EditablePick } from "./Editable"; +import { LayerPolylineView } from "../layers/LayerPolylineView"; +import { LayerName } from "../layers/Layers"; +import { Selection, SelectType } from "../layers/Selection"; export class Point { public constructor(x: number, y: number) { @@ -37,8 +37,8 @@ class PointSegmentResult { export class DrawablePolylinePack implements PrimitivePack { public constructor(points: Point[], closed: boolean, lineWidth: Size, - fill: boolean, fillColorName: string, fillAlphaName: string, - stroke: boolean, strokeColorName: string, strokeAlphaName: string) { + fill: boolean, fillColorName: string, fillAlphaName: string, + stroke: boolean, strokeColorName: string, strokeAlphaName: string) { this.points = points; this.closed = closed; this.lineWidth = lineWidth; @@ -224,7 +224,7 @@ export class DrawablePolylineEditor { } } public rotateCW(centerX?: number, centerY?: number) { - if(centerX === undefined || centerY === undefined) { + if (centerX === undefined || centerY === undefined) { let center = this.polyline.calculator.aabbCenter(); centerX = center.x; centerY = center.y; @@ -237,7 +237,7 @@ export class DrawablePolylineEditor { } } public rotateCCW(centerX?: number, centerY?: number) { - if(centerX === undefined || centerY === undefined) { + if (centerX === undefined || centerY === undefined) { let center = this.polyline.calculator.aabbCenter(); centerX = center.x; centerY = center.y; @@ -461,7 +461,7 @@ export class DrawablePolylineEditUi { } render(canvas: Canvas, map: Map): TemplateResult { - return html``; + return html``; } } @@ -545,7 +545,7 @@ export class DrawablePolyline implements EditablePick, EditableDeleteClone, Edit pack.points = points; return pack; } - public cloneOnCanvas(canvas:Canvas, offsetX: number, offsetY: number): Drawable { + public cloneOnCanvas(canvas: Canvas, offsetX: number, offsetY: number): Drawable { if (!this.check()) return undefined; let layerView = canvas.findLayer(LayerName.POLYLINE_VIEW); let newPolyline = new DrawablePolyline(this.clone(offsetX, offsetY)); diff --git a/ts/editable/DrawablePolylineEditElement.ts b/ts/editable/DrawablePolylineEditElement.ts index 2b35b9d..d5fa8f9 100644 --- a/ts/editable/DrawablePolylineEditElement.ts +++ b/ts/editable/DrawablePolylineEditElement.ts @@ -5,27 +5,37 @@ import { Map } from "../data/Map"; import { AlphaEntry, ColorEntry } from "../util/Color"; import "../elements/ColorAlphaElement" import "../elements/TriStateCheckboxElement" +import "../elements/NumberInputElement" import { Selection, SelectType } from "../layers/Selection"; import { rotateCCWIcon, rotateCWIcon, flipXIcon, flipYIcon } from "../util/Icons"; +import { TriState, getTriState, getUnifiedValue } from "../util/MultiSelect"; @customElement('polylineedit-element') export class PolylineEdit extends LitElement { @property() - polyline: DrawablePolyline; + polylines: DrawablePolyline[] = []; @property() canvas: Canvas; @property() map: Map; + private get firstPolyline(): DrawablePolyline | undefined { + return this.polylines.length > 0 ? this.polylines[0] : undefined; + } + deletePolyline() { - this.polyline.deleteOnCanvas(this.canvas); + for (const polyline of this.polylines) { + polyline.deleteOnCanvas(this.canvas); + } } copyPolyline() { let offset = this.canvas.getCamera().screenSizeToCanvas(20); - this.polyline.cloneOnCanvas(this.canvas, offset, offset); - Selection.select(SelectType.POLYLINE, this.polyline); + for (const polyline of this.polylines) { + polyline.cloneOnCanvas(this.canvas, offset, offset); + } + Selection.select(SelectType.POLYLINE, this.polylines[0]); } @property() @@ -33,52 +43,96 @@ export class PolylineEdit extends LitElement { calcArea() { let width = this.map.widthMillimeter; let height = this.map.heightMillimeter; - let unit = this.polyline.style.fill ? "mm²" : "mm"; + let unit = "mm"; if (!(this.map.widthMillimeter > 0 && this.map.heightMillimeter > 0)) { width = this.map.width; height = this.map.height; unit = "pixels" } - if (this.polyline.style.fill) { - let area = this.polyline.calculator.area(); - let areaMM2 = area / this.map.width / this.map.height * width * height; - areaMM2 = Math.round(areaMM2 * 100) / 100; - this.area = areaMM2 + unit; - } else { - let length = this.polyline.calculator.length(); - let lengthMM = length * Math.sqrt(width * height / this.map.width / this.map.height); - lengthMM = Math.round(lengthMM * 100) / 100; - this.area = lengthMM + unit; + let totalValue = 0; + for (const polyline of this.polylines) { + if (polyline.style.fill) { + let area = polyline.calculator.area(); + let areaMM2 = area / this.map.width / this.map.height * width * height; + totalValue += areaMM2; + unit = "mm²"; + } else { + let length = polyline.calculator.length(); + let lengthMM = length * Math.sqrt(width * height / this.map.width / this.map.height); + totalValue += lengthMM; + } } + totalValue = Math.round(totalValue * 100) / 100; + this.area = totalValue + unit; } rotateCCW() { - this.polyline.editor.rotateCCW(); + for (const polyline of this.polylines) { + polyline.editor.rotateCCW(); + } this.canvas.requestRender(); } rotateCW() { - this.polyline.editor.rotateCW(); + for (const polyline of this.polylines) { + polyline.editor.rotateCW(); + } this.canvas.requestRender(); } flipX() { - this.polyline.editor.flipX(); + for (const polyline of this.polylines) { + polyline.editor.flipX(); + } this.canvas.requestRender(); } flipY() { - this.polyline.editor.flipY(); + for (const polyline of this.polylines) { + polyline.editor.flipY(); + } this.canvas.requestRender(); } - private onStyleCheck = (ev: Event, options: { fill?: boolean, stroke?: boolean, closed?: boolean }) => { - if (options.fill !== undefined) this.polyline.style.fill = options.fill; - if (options.stroke !== undefined) this.polyline.style.stroke = options.stroke; - if (options.closed !== undefined) this.polyline.style.closed = options.closed; + private getFillState(): TriState { + return getTriState(this.polylines, p => p.style.fill); + } + private getStrokeState(): TriState { + return getTriState(this.polylines, p => p.style.stroke); + } + private getClosedState(): TriState { + return getTriState(this.polylines, p => p.style.closed); + } + private getStrokeColor(): ColorEntry | undefined { + return getUnifiedValue(this.polylines, p => p.style.strokeColor); + } + private getStrokeAlpha(): AlphaEntry | undefined { + return getUnifiedValue(this.polylines, p => p.style.strokeAlpha); + } + private getFillColor(): ColorEntry | undefined { + return getUnifiedValue(this.polylines, p => p.style.fillColor); + } + private getFillAlpha(): AlphaEntry | undefined { + return getUnifiedValue(this.polylines, p => p.style.fillAlpha); + } + private getOnScreen(): number | undefined { + return getUnifiedValue(this.polylines, p => p.style.onScreen); + } + private getOnCanvas(): number | undefined { + return getUnifiedValue(this.polylines, p => p.style.onCanvas); + } + + private onStyleCheck = (options: { fill?: boolean, stroke?: boolean, closed?: boolean }) => { + for (const polyline of this.polylines) { + if (options.fill !== undefined) polyline.style.fill = options.fill; + if (options.stroke !== undefined) polyline.style.stroke = options.stroke; + if (options.closed !== undefined) polyline.style.closed = options.closed; + } this.canvas.requestRender(); this.performUpdate(); }; - private onSizeInput = (ev: Event, options: { screen?: string, canvas?: string }) => { - if (options.screen !== undefined) this.polyline.style.onScreen = parseInt(options.screen); - if (options.canvas !== undefined) this.polyline.style.onCanvas = parseInt(options.canvas); + private onSizeInput = (options: { screen?: string, canvas?: string }) => { + for (const polyline of this.polylines) { + if (options.screen !== undefined) polyline.style.onScreen = parseInt(options.screen); + if (options.canvas !== undefined) polyline.style.onCanvas = parseInt(options.canvas); + } this.canvas.requestRender(); this.performUpdate(); }; @@ -100,55 +154,71 @@ export class PolylineEdit extends LitElement {
Stroke Color
{ - this.polyline.style.setStrokeColor(color, undefined); + for (const polyline of this.polylines) { + polyline.style.setStrokeColor(color, undefined); + } this.canvas.requestRender(); }} .setAlpha=${(alpha: AlphaEntry) => { - this.polyline.style.setStrokeColor(undefined, alpha); + for (const polyline of this.polylines) { + polyline.style.setStrokeColor(undefined, alpha); + } this.canvas.requestRender(); }} >
Fill Color
{ - this.polyline.style.setFillColor(color, undefined); + for (const polyline of this.polylines) { + polyline.style.setFillColor(color, undefined); + } this.canvas.requestRender(); }} .setAlpha=${(alpha: AlphaEntry) => { - this.polyline.style.setFillColor(undefined, alpha); + for (const polyline of this.polylines) { + polyline.style.setFillColor(undefined, alpha); + } this.canvas.requestRender(); }} >
- this.onSizeInput(ev, { screen: (ev.target).value })}> +
- this.onSizeInput(ev, { canvas: (ev.target).value })}> +
`; diff --git a/ts/editable/DrawableText.ts b/ts/editable/DrawableText.ts index f49d6ba..68cfbb4 100644 --- a/ts/editable/DrawableText.ts +++ b/ts/editable/DrawableText.ts @@ -1,16 +1,16 @@ -import {Drawable} from "../drawable/Drawable"; -import {Canvas} from "../Canvas"; -import {Renderer} from "../Renderer"; -import {Camera} from "../Camera"; -import {Size} from "../util/Size"; -import {AlphaEntry, ColorEntry, combineColorAlpha} from "../util/Color"; -import {AABB} from "../util/AABB"; -import {html, TemplateResult} from "lit-html"; -import {Primitive, PrimitivePack} from "./Primitive"; -import {EditableColor, EditableDeleteClone, EditableMove, EditablePick} from "./Editable"; -import {LayerTextView} from "../layers/LayerTextView"; -import {LayerName} from "../layers/Layers"; -import {Selection, SelectType} from "../layers/Selection"; +import { Drawable } from "../drawable/Drawable"; +import { Canvas } from "../Canvas"; +import { Renderer } from "../Renderer"; +import { Camera } from "../Camera"; +import { Size } from "../util/Size"; +import { AlphaEntry, ColorEntry, combineColorAlpha } from "../util/Color"; +import { AABB } from "../util/AABB"; +import { html, TemplateResult } from "lit-html"; +import { Primitive, PrimitivePack } from "./Primitive"; +import { EditableColor, EditableDeleteClone, EditableMove, EditablePick } from "./Editable"; +import { LayerTextView } from "../layers/LayerTextView"; +import { LayerName } from "../layers/Layers"; +import { Selection, SelectType } from "../layers/Selection"; export class DrawableTextPack implements PrimitivePack { public constructor(text: string, colorName: string, alphaName: string, fontSize: Size, x: number, y: number) { @@ -225,7 +225,7 @@ export class DrawableText implements EditablePick, EditableDeleteClone, Editable if (!this.sizeValid || this.canvasZoom != camera.getZoom()) { this.sizeValid = true; - let {width, totalHeight, fontSize} = renderer.measureText(camera, this._text, this.fontSize); + let { width, totalHeight, fontSize } = renderer.measureText(camera, this._text, this.fontSize); let ratio = camera.screenSizeToCanvas(1); this.canvasZoom = camera.getZoom(); this.canvasWidth = width * ratio / 2; @@ -253,6 +253,6 @@ export class DrawableText implements EditablePick, EditableDeleteClone, Editable } public renderUi(canvas: Canvas): TemplateResult { - return html``; + return html``; } } diff --git a/ts/editable/DrawableTextEditElement.ts b/ts/editable/DrawableTextEditElement.ts index 3e2aa0b..71ad89f 100644 --- a/ts/editable/DrawableTextEditElement.ts +++ b/ts/editable/DrawableTextEditElement.ts @@ -3,36 +3,68 @@ import { DrawableText } from "./DrawableText"; import { Canvas } from "../Canvas"; import { AlphaEntry, ColorEntry } from "../util/Color"; import "../elements/ColorAlphaElement" +import "../elements/NumberInputElement" +import "../elements/TextInputElement" import { Selection, SelectType } from "../layers/Selection"; +import { getUnifiedValue } from "../util/MultiSelect"; @customElement('textedit-element') export class TextEdit extends LitElement { @property() - text: DrawableText; + texts: DrawableText[] = []; @property() canvas: Canvas; + private get firstText(): DrawableText | undefined { + return this.texts.length > 0 ? this.texts[0] : undefined; + } + deleteText() { - this.text.deleteOnCanvas(this.canvas); + for (const text of this.texts) { + text.deleteOnCanvas(this.canvas); + } } copyText() { let offset = this.canvas.getCamera().screenSizeToCanvas(20); - this.text.cloneOnCanvas(this.canvas, offset, offset); - Selection.select(SelectType.TEXT, this.text); + for (const text of this.texts) { + text.cloneOnCanvas(this.canvas, offset, offset); + } + Selection.select(SelectType.TEXT, this.texts[0]); + } + + private getText(): string | undefined { + return getUnifiedValue(this.texts, t => t.text); + } + private getColor(): ColorEntry | undefined { + return getUnifiedValue(this.texts, t => t.color); + } + private getAlpha(): AlphaEntry | undefined { + return getUnifiedValue(this.texts, t => t.alpha); + } + private getOnScreen(): number | undefined { + return getUnifiedValue(this.texts, t => t.onScreen); + } + private getOnCanvas(): number | undefined { + return getUnifiedValue(this.texts, t => t.onCanvas); } private editText = (content: string) => { - if (!content.length) content = "text"; - this.text.text = content; + for (const text of this.texts) { + if (content.length) { + text.text = content; + } + } this.canvas.requestRender(); this.performUpdate(); }; - private onSizeInput = (ev: Event, options: { screen?: string, canvas?: string }) => { - if (options.screen !== undefined) this.text.onScreen = parseInt(options.screen); - if (options.canvas !== undefined) this.text.onCanvas = parseInt(options.canvas); + private onSizeInput = (options: { screen?: string, canvas?: string }) => { + for (const text of this.texts) { + if (options.screen !== undefined) text.onScreen = parseInt(options.screen); + if (options.canvas !== undefined) text.onCanvas = parseInt(options.canvas); + } this.canvas.requestRender(); this.performUpdate(); }; @@ -43,28 +75,45 @@ export class TextEdit extends LitElement {
Text
- this.editText((ev.target).value)}>
+ +
Text Color
{ - this.text.setColorAlpha(color, undefined); + for (const text of this.texts) { + text.setColorAlpha(color, undefined); + } this.canvas.requestRender(); }} .setAlpha=${(alpha: AlphaEntry) => { - this.text.setColorAlpha(undefined, alpha); + for (const text of this.texts) { + text.setColorAlpha(undefined, alpha); + } this.canvas.requestRender(); }} >
- this.onSizeInput(ev, { screen: (ev.target).value })}> +
- this.onSizeInput(ev, { canvas: (ev.target).value })}> +
`; diff --git a/ts/elements/ColorAlphaElement.ts b/ts/elements/ColorAlphaElement.ts index 88d6521..048bc13 100644 --- a/ts/elements/ColorAlphaElement.ts +++ b/ts/elements/ColorAlphaElement.ts @@ -9,9 +9,29 @@ export class ColorAlphaElement extends LitElement { @property() private setAlpha: (alpha: AlphaEntry) => void; @property() - private currentColor: ColorEntry; + private currentColor: ColorEntry | undefined = undefined; @property() - private currentAlpha: AlphaEntry; + private currentAlpha: AlphaEntry | undefined = undefined; + + private isSelected(color: ColorEntry): boolean { + return this.currentColor !== undefined && this.currentColor.name === color.name; + } + + private isAlphaSelected(alpha: AlphaEntry): boolean { + return this.currentAlpha !== undefined && this.currentAlpha.value === alpha.value; + } + + private getColorButtonClass(color: ColorEntry): string { + const selected = this.isSelected(color) ? 'selected' : ''; + const empty = this.currentColor === undefined ? 'empty' : ''; + return `configColorButton ${selected} ${empty}`.trim(); + } + + private getAlphaButtonClass(alpha: AlphaEntry): string { + const selected = this.isAlphaSelected(alpha) ? 'selected' : ''; + const empty = this.currentAlpha === undefined ? 'empty' : ''; + return `configAlphaButton ${selected} ${empty}`.trim(); + } render() { return html` @@ -24,13 +44,16 @@ export class ColorAlphaElement extends LitElement { outline: 3px solid #007bff; outline-offset: -3px; } + .configColorButton.empty, .configAlphaButton.empty { + opacity: 0.5; + }
- ${ColorEntry.list.map(color => html``)} + ${ColorEntry.list.map(color => html``)}
${AlphaEntry.list.map(alpha => { let color = 255 * (1 - alpha.value); - return html`` + return html`` })}
`; diff --git a/ts/elements/NumberInputElement.ts b/ts/elements/NumberInputElement.ts new file mode 100644 index 0000000..0a46b90 --- /dev/null +++ b/ts/elements/NumberInputElement.ts @@ -0,0 +1,97 @@ +import { css, customElement, html, LitElement, property } from 'lit-element'; + +@customElement('number-input') +export class NumberInputElement extends LitElement { + + @property() + value: number | null | undefined = null; + + @property() + placeholder: string = '-'; + + @property() + min: number | undefined; + + @property() + max: number | undefined; + + @property() + onChange: (value: number) => void; + + private isEmpty(): boolean { + return this.value === null || this.value === undefined; + } + + private handleInput(e: Event) { + const input = e.target as HTMLInputElement; + const val = input.value.trim(); + + if (val === '') { + this.value = this.min ?? 0; + } else { + const num = parseFloat(val); + if (!isNaN(num)) { + this.value = num; + } + } + + if (this.onChange) { + this.onChange(this.value!); + } + } + + static override styles = css` + :host { + display: inline-flex; + align-items: center; + vertical-align: middle; + } + + input { + width: 60px; + padding: 2px 4px; + border: 1px solid #666; + border-radius: 3px; + background: #fff; + color: #333; + font-size: 12px; + text-align: center; + -moz-appearance: textfield; + } + + // input::-webkit-outer-spin-button, + // input::-webkit-inner-spin-button { + // -webkit-appearance: none; + // margin: 0; + // } + + input:focus { + outline: none; + border-color: #4a90d9; + } + + input.empty { + color: #999; + } + + input::placeholder { + color: #999; + } + `; + + render() { + const displayValue = this.isEmpty() ? '-' : String(this.value); + + return html` + + `; + } +} diff --git a/ts/elements/TextInputElement.ts b/ts/elements/TextInputElement.ts new file mode 100644 index 0000000..4df669c --- /dev/null +++ b/ts/elements/TextInputElement.ts @@ -0,0 +1,73 @@ +import { css, customElement, html, LitElement, property } from 'lit-element'; + +@customElement('text-input') +export class TextInputElement extends LitElement { + + @property() + value: string | null | undefined = null; + + @property() + placeholder: string = '-'; + + @property() + onChange: (value: string) => void; + + private isEmpty(): boolean { + return this.value === null || this.value === undefined; + } + + private handleInput(e: Event) { + const input = e.target as HTMLInputElement; + const val = input.value; + this.value = val; + + if (this.onChange) { + this.onChange(val); + } + } + + static override styles = css` + :host { + display: inline-flex; + align-items: center; + vertical-align: middle; + } + + input { + width: 120px; + padding: 2px 4px; + border: 1px solid #666; + border-radius: 3px; + background: #fff; + color: #333; + font-size: 12px; + } + + input:focus { + outline: none; + border-color: #4a90d9; + } + + input.empty { + color: #999; + } + + input::placeholder { + color: #999; + } + `; + + render() { + const displayValue = this.isEmpty() ? '' : String(this.value); + + return html` + + `; + } +} diff --git a/ts/util/MultiSelect.ts b/ts/util/MultiSelect.ts new file mode 100644 index 0000000..c9de32c --- /dev/null +++ b/ts/util/MultiSelect.ts @@ -0,0 +1,25 @@ +export type TriState = 'none' | 'some' | 'all'; +export type UnifiedValue = T | undefined; + +export function getUnifiedValue(arr: T[], getter: (item: T) => V): UnifiedValue { + if (arr.length === 0) return undefined; + const first = getter(arr[0]); + for (const item of arr) { + if (getter(item) !== first) return undefined; + } + return getter(arr[0]); +} + +export function getTriState(arr: T[], getter: (item: T) => boolean): TriState { + if (arr.length === 0) return 'none'; + const first = getter(arr[0]); + for (const item of arr) { + if (getter(item) !== first) return 'some'; + } + return first ? 'all' : 'none'; +} + +export function boolFromTriState(state: TriState): boolean | undefined { + if (state === 'some') return undefined; + return state === 'all'; +} From 7a9aaa0f95584d9281c8a3310c22d623bedfcda7 Mon Sep 17 00:00:00 2001 From: misdake Date: Sat, 28 Mar 2026 12:18:28 +0800 Subject: [PATCH 11/12] Use PolylineEdit/TextEdit for MULTIPLE selection; fix clone/delete selection state --- ts/App.ts | 18 ++++++++++++++++-- ts/editable/DrawablePolylineEditElement.ts | 20 ++++++++++++++++++-- ts/editable/DrawableTextEditElement.ts | 20 ++++++++++++++++++-- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/ts/App.ts b/ts/App.ts index 4db7993..66c6a12 100644 --- a/ts/App.ts +++ b/ts/App.ts @@ -7,7 +7,6 @@ import "./elements/SelectElement"; import "./elements/TitleElement"; import "./editable/DrawablePolylineEditElement"; import "./editable/DrawableTextEditElement"; -import "./editable/DrawableMultipleEditElement"; import { Selection, SelectType } from "./layers/Selection"; import { DrawablePolyline, DrawablePolylinePack } from "./editable/DrawablePolyline"; import { DrawableText, DrawableTextPack } from "./editable/DrawableText"; @@ -130,7 +129,22 @@ class App { }); Selection.register(SelectType.MULTIPLE, (item: Drawable[]) => { - render(MultipleEdit.renderUi(canvas, item), document.getElementById("panelSelected")); + const polylines: DrawablePolyline[] = []; + const texts: DrawableText[] = []; + for (const d of item) { + if (d instanceof DrawablePolyline) { + polylines.push(d); + } else if (d instanceof DrawableText) { + texts.push(d); + } + } + const polylinePanel = polylines.length > 0 + ? html`` + : html``; + const textPanel = texts.length > 0 + ? html`` + : html``; + render(html`${polylinePanel}${textPanel}`, document.getElementById("panelSelected")); canvas.enterEditors(EditorName.CAMERA_CONTROL, EditorName.SELECT, EditorName.MULTIPLE_EDIT); }, () => { render(html``, document.getElementById("panelSelected")); diff --git a/ts/editable/DrawablePolylineEditElement.ts b/ts/editable/DrawablePolylineEditElement.ts index d5fa8f9..0d028e9 100644 --- a/ts/editable/DrawablePolylineEditElement.ts +++ b/ts/editable/DrawablePolylineEditElement.ts @@ -29,13 +29,29 @@ export class PolylineEdit extends LitElement { for (const polyline of this.polylines) { polyline.deleteOnCanvas(this.canvas); } + if (this.polylines.length > 1) { + Selection.deselectAny(); + } else { + Selection.deselect(SelectType.POLYLINE); + } } copyPolyline() { let offset = this.canvas.getCamera().screenSizeToCanvas(20); + const newPolylines: DrawablePolyline[] = []; for (const polyline of this.polylines) { - polyline.cloneOnCanvas(this.canvas, offset, offset); + const cloned = polyline.cloneOnCanvas(this.canvas, offset, offset) as DrawablePolyline; + if (cloned) newPolylines.push(cloned); + } + if (newPolylines.length > 0) { + if (this.polylines.length > 1) { + const selected = Selection.getSelected(); + if (Array.isArray(selected.item)) { + (selected.item as DrawablePolyline[]).splice(0, selected.item.length, ...newPolylines); + } + } else { + Selection.select(SelectType.POLYLINE, newPolylines[0]); + } } - Selection.select(SelectType.POLYLINE, this.polylines[0]); } @property() diff --git a/ts/editable/DrawableTextEditElement.ts b/ts/editable/DrawableTextEditElement.ts index 71ad89f..9f5272b 100644 --- a/ts/editable/DrawableTextEditElement.ts +++ b/ts/editable/DrawableTextEditElement.ts @@ -25,13 +25,29 @@ export class TextEdit extends LitElement { for (const text of this.texts) { text.deleteOnCanvas(this.canvas); } + if (this.texts.length > 1) { + Selection.deselectAny(); + } else { + Selection.deselect(SelectType.TEXT); + } } copyText() { let offset = this.canvas.getCamera().screenSizeToCanvas(20); + const newTexts: DrawableText[] = []; for (const text of this.texts) { - text.cloneOnCanvas(this.canvas, offset, offset); + const cloned = text.cloneOnCanvas(this.canvas, offset, offset) as DrawableText; + if (cloned) newTexts.push(cloned); + } + if (newTexts.length > 0) { + if (this.texts.length > 1) { + const selected = Selection.getSelected(); + if (Array.isArray(selected.item)) { + (selected.item as DrawableText[]).splice(0, selected.item.length, ...newTexts); + } + } else { + Selection.select(SelectType.TEXT, newTexts[0]); + } } - Selection.select(SelectType.TEXT, this.texts[0]); } private getText(): string | undefined { From e1737721dea54030cec614f6edc4e5757ebd9a9b Mon Sep 17 00:00:00 2001 From: misdake Date: Sat, 28 Mar 2026 13:10:36 +0800 Subject: [PATCH 12/12] fix: improve drag handling and panel event blocking --- dist/index.html | 13 +++-- dist/style.css | 9 ++++ ts/Canvas.ts | 53 ++++++++++++++----- ts/editable/DrawablePolyline.ts | 90 +++++++++++++++++++++++++++++++++ ts/editors/EditorSelect.ts | 8 +-- 5 files changed, 148 insertions(+), 25 deletions(-) diff --git a/dist/index.html b/dist/index.html index 1b13c70..9971913 100644 --- a/dist/index.html +++ b/dist/index.html @@ -11,9 +11,9 @@
-
+
-
+
@@ -36,8 +36,8 @@
-
-