-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
diff --git a/dist/res/github.png b/dist/res/github.png
index 2752756..d44393a 100644
Binary files a/dist/res/github.png and b/dist/res/github.png differ
diff --git a/dist/res/refresh.png b/dist/res/refresh.png
index 710f506..2a757a1 100644
Binary files a/dist/res/refresh.png and b/dist/res/refresh.png differ
diff --git a/dist/style.css b/dist/style.css
index 0d3c6a3..e111f9c 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: 24px;
+ padding: 0;
+ 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,137 +53,607 @@ 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;
+ user-select: none;
+ -webkit-user-select: none;
}
.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);
+.imageSource {
+ margin-left: 4px;
+ color: var(--text-secondary);
font-size: 12px;
}
-#panel {
- display: none;
- float: left;
- width: 210px;
- height: 100%;
- background: white;
- flex-flow: column;
+.imageSourceLabel {
+ color: var(--text-secondary);
+ font-size: 12px;
}
-#panelScroll {
+#container {
+ position: relative;
+ width: 100%;
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 - 100px);
overflow-y: auto;
- flex: 1 1 auto;
}
-#container {
- position: relative;
+.ui-panel.dragging {
+ pointer-events: none;
+}
+
+.ui-panel {
+ user-select: none;
+ -webkit-user-select: none;
+}
+
+#propertyPanel::-webkit-scrollbar {
+ width: 6px;
+}
+
+#propertyPanel::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 3px;
+}
+
+#propertyPanel::-webkit-scrollbar-thumb {
+ background: var(--primary-color);
+ border-radius: 3px;
+}
+
+#creationButtons {
display: flex;
- height: 100%;
+ gap: 4px;
+ flex-wrap: wrap;
+ margin: 10px 0;
+}
+
+#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;
+ 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 {
- /*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;
+}
+
+.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: 5px 10px;
+ margin: 8px 0;
+ padding: 12px;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+}
+
+.sizeInput {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 6px 0;
+}
+
+.sizeInput label {
+ font-size: 13px;
+ color: var(--text-secondary);
+ white-space: nowrap;
}
-.colorAlphaContainerSplit {
- width: 0px;
- display: inline-block;
+.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 {
- 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: 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%;
+ 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 {
- 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;
- margin-bottom: 3px;
+ 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 {
cursor: pointer;
display: flex;
flex-flow: row;
-}
-#footer a {
+ align-items: center;
+ color: var(--text-secondary);
+ 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 {
- margin-left: 2px;
+ margin-left: 6px;
cursor: pointer;
line-height: 24px;
vertical-align: middle;
}
.footerIcon {
- width: 24px;
- height: 24px;
+ width: 20px;
+ height: 20px;
+ 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;
+ 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) {
+ .configButton {
+ padding: 8px 12px;
+ font-size: 12px;
+ }
+
+ #selectPanel,
+ #propertyPanel,
+ #cameraPanel {
+ 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;
+ }
}
\ No newline at end of file
diff --git a/ts/App.ts b/ts/App.ts
index 887d4cc..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";
@@ -24,12 +23,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 +72,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);
@@ -112,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"));
@@ -167,11 +199,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/Canvas.ts b/ts/Canvas.ts
index 6f407a2..c2044b8 100644
--- a/ts/Canvas.ts
+++ b/ts/Canvas.ts
@@ -1,16 +1,16 @@
-import {Layer} from "./layers/Layer";
-import {Map} from "./data/Map";
-import {Renderer} from "./Renderer";
-import {Camera} from "./Camera";
-import {Data} from "./data/Data";
-import {Ui} from "./util/Ui";
+import { Layer } from "./layers/Layer";
+import { Map } from "./data/Map";
+import { Renderer } from "./Renderer";
+import { Camera } from "./Camera";
+import { Data } from "./data/Data";
+import { Ui } from "./util/Ui";
import "hammerjs";
-import {html, render} from "lit-html";
+import { html, render } from "lit-html";
import "./elements/ZoomElement"
-import {LayerName} from "./layers/Layers";
-import {Editor, UsageType} from "./editors/Editor";
-import {EditorName} from "./editors/Editors";
-import {Env} from "./Env";
+import { LayerName } from "./layers/Layers";
+import { Editor, UsageType } from "./editors/Editor";
+import { EditorName } from "./editors/Editors";
+import { Env } from "./Env";
export class Canvas {
private readonly domElement: HTMLElement;
@@ -21,6 +21,7 @@ export class Canvas {
private width: number;
private height: number;
+ private _isDragging: boolean = false;
public constructor(domElement: HTMLElement, id: string) {
this.domElement = domElement;
@@ -50,6 +51,7 @@ export class Canvas {
}
public init(): void {
+ let self = this;
let convertMouseEvent = (event: MouseEvent) => {
return {
button: event.button,
@@ -118,13 +120,39 @@ export class Canvas {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
+ document.querySelectorAll(".ui-panel").forEach(p => p.classList.add("dragging"));
+ self._isDragging = true;
let length = this.currentEditors.length;
for (let i = length - 1; i >= 0; i--) {
let editor = this.currentEditors[i];
if (editor.mouseListener && editor.mouseListener.onmousedown(e)) break;
}
+
+ let onDocMouseMove = (docEvent: MouseEvent) => {
+ let docE = convertMouseEvent(docEvent);
+ let len = self.currentEditors.length;
+ for (let i = len - 1; i >= 0; i--) {
+ let editor = self.currentEditors[i];
+ if (editor.mouseListener && editor.mouseListener.onmousemove(docE)) break;
+ }
+ };
+ let onDocMouseUp = (docEvent: MouseEvent) => {
+ document.removeEventListener('mousemove', onDocMouseMove);
+ document.removeEventListener('mouseup', onDocMouseUp);
+ document.querySelectorAll(".ui-panel").forEach(p => p.classList.remove("dragging"));
+ self._isDragging = false;
+ let docE = convertMouseEvent(docEvent);
+ let len = self.currentEditors.length;
+ for (let i = len - 1; i >= 0; i--) {
+ let editor = self.currentEditors[i];
+ if (editor.mouseListener && editor.mouseListener.onmouseup(docE)) break;
+ }
+ };
+ document.addEventListener('mousemove', onDocMouseMove);
+ document.addEventListener('mouseup', onDocMouseUp);
};
this.canvasElement.onmouseup = event => {
+ if (self._isDragging) return;
let e = convertMouseEvent(event);
event.preventDefault();
event.stopPropagation();
@@ -136,6 +164,7 @@ export class Canvas {
}
};
this.canvasElement.onmousemove = event => {
+ if (self._isDragging) return;
let e = convertMouseEvent(event);
event.preventDefault();
event.stopPropagation();
@@ -197,7 +226,7 @@ export class Canvas {
if (Ui.isMobile()) {
let hammer = new Hammer(this.canvasElement);
- hammer.get('pan').set({direction: Hammer.DIRECTION_ALL});
+ hammer.get('pan').set({ direction: Hammer.DIRECTION_ALL });
hammer.on("pan", (event: HammerInput) => {
event.deltaX *= window.devicePixelRatio;
event.deltaY *= window.devicePixelRatio;
diff --git a/ts/editable/DrawableMultipleEditElement.ts b/ts/editable/DrawableMultipleEditElement.ts
index 282ab0e..23edf2e 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/DrawablePolyline.ts b/ts/editable/DrawablePolyline.ts
index 7ee3e26..c24dc76 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;
@@ -339,6 +339,96 @@ export class DrawablePolylineCalculator {
}
return (newX === xy.x && newY === xy.y) ? undefined : new Point(newX, newY);
}
+
+ public intersectsAABB(minX: number, minY: number, maxX: number, maxY: number): boolean {
+ if (this.points.length === 0) return false;
+
+ const aabbMinX = Math.min(minX, maxX);
+ const aabbMaxX = Math.max(minX, maxX);
+ const aabbMinY = Math.min(minY, maxY);
+ const aabbMaxY = Math.max(minY, maxY);
+
+ for (const point of this.points) {
+ if (point.x >= aabbMinX && point.x <= aabbMaxX && point.y >= aabbMinY && point.y <= aabbMaxY) {
+ return true;
+ }
+ }
+
+ if (this.polyline.style.closed && this.points.length >= 3) {
+ const corners = [
+ new Point(aabbMinX, aabbMinY),
+ new Point(aabbMaxX, aabbMinY),
+ new Point(aabbMaxX, aabbMaxY),
+ new Point(aabbMinX, aabbMaxY)
+ ];
+ for (const corner of corners) {
+ if (this.pointInPolygon(corner)) {
+ return true;
+ }
+ }
+ }
+
+ const polylineEdges = this.points.length;
+ const closedOffset = this.polyline.style.closed ? 1 : 0;
+
+ for (let i = 0; i < polylineEdges - 1 + closedOffset; i++) {
+ const p1 = this.points[i];
+ const p2 = this.points[(i + 1) % this.points.length];
+
+ if (this.lineIntersectsAABB(p1.x, p1.y, p2.x, p2.y, aabbMinX, aabbMinY, aabbMaxX, aabbMaxY)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private lineIntersectsAABB(x1: number, y1: number, x2: number, y2: number, minX: number, minY: number, maxX: number, maxY: number): boolean {
+ if (x1 >= minX && x1 <= maxX && y1 >= minY && y1 <= maxY) return true;
+ if (x2 >= minX && x2 <= maxX && y2 >= minY && y2 <= maxY) return true;
+
+ const edges = [
+ [minX, minY, maxX, minY],
+ [maxX, minY, maxX, maxY],
+ [maxX, maxY, minX, maxY],
+ [minX, maxY, minX, minY]
+ ];
+
+ for (const edge of edges) {
+ if (this.lineSegmentsIntersect(x1, y1, x2, y2, edge[0], edge[1], edge[2], edge[3])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private lineSegmentsIntersect(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): boolean {
+ const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
+ if (Math.abs(denom) < 1e-10) return false;
+
+ const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
+ const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
+
+ return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
+ }
+
+ private pointInPolygon(point: Point): boolean {
+ if (this.points.length < 3) return false;
+
+ let inside = false;
+ const n = this.points.length;
+ for (let i = 0, j = n - 1; i < n; j = i++) {
+ const xi = this.points[i].x, yi = this.points[i].y;
+ const xj = this.points[j].x, yj = this.points[j].y;
+
+ if (((yi > point.y) !== (yj > point.y)) &&
+ (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi)) {
+ inside = !inside;
+ }
+ }
+ return inside;
+ }
}
export class DrawablePolylineStyle {
@@ -461,7 +551,7 @@ export class DrawablePolylineEditUi {
}
render(canvas: Canvas, map: Map): TemplateResult {
- return html`
`;
+ return html`
`;
}
}
@@ -545,7 +635,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 69b7060..0d028e9 100644
--- a/ts/editable/DrawablePolylineEditElement.ts
+++ b/ts/editable/DrawablePolylineEditElement.ts
@@ -1,29 +1,57 @@
-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 "../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);
+ }
+ if (this.polylines.length > 1) {
+ Selection.deselectAny();
+ } else {
+ Selection.deselect(SelectType.POLYLINE);
+ }
}
copyPolyline() {
let offset = this.canvas.getCamera().screenSizeToCanvas(20);
- this.polyline.cloneOnCanvas(this.canvas, offset, offset);
- Selection.select(SelectType.POLYLINE, this.polyline);
+ const newPolylines: DrawablePolyline[] = [];
+ for (const polyline of this.polylines) {
+ 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]);
+ }
+ }
}
@property()
@@ -31,102 +59,184 @@ 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 = "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();
};
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();
- }}
+ for (const polyline of this.polylines) {
+ polyline.style.setStrokeColor(color, undefined);
+ }
+ this.canvas.requestRender();
+ }}
.setAlpha=${(alpha: AlphaEntry) => {
- this.polyline.style.setStrokeColor(undefined, alpha);
- this.canvas.requestRender();
- }}
+ for (const polyline of this.polylines) {
+ polyline.style.setStrokeColor(undefined, alpha);
+ }
+ this.canvas.requestRender();
+ }}
>
- fillColor
+ Fill Color
{
- this.polyline.style.setFillColor(color, undefined);
- this.canvas.requestRender();
- }}
+ for (const polyline of this.polylines) {
+ polyline.style.setFillColor(color, undefined);
+ }
+ this.canvas.requestRender();
+ }}
.setAlpha=${(alpha: AlphaEntry) => {
- this.polyline.style.setFillColor(undefined, alpha);
- this.canvas.requestRender();
- }}
+ for (const polyline of this.polylines) {
+ 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
+
+
+
+
+
+
+
+
`;
}
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 e421ada..9f5272b 100644
--- a/ts/editable/DrawableTextEditElement.ts
+++ b/ts/editable/DrawableTextEditElement.ts
@@ -1,64 +1,137 @@
-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 "../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);
+ }
+ if (this.texts.length > 1) {
+ Selection.deselectAny();
+ } else {
+ Selection.deselect(SelectType.TEXT);
+ }
}
copyText() {
let offset = this.canvas.getCamera().screenSizeToCanvas(20);
- this.text.cloneOnCanvas(this.canvas, offset, offset);
- Selection.select(SelectType.TEXT, this.text);
+ const newTexts: DrawableText[] = [];
+ for (const text of this.texts) {
+ 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]);
+ }
+ }
+ }
+
+ 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();
};
render() {
return html`
-
-
+
+
- text
- this.editText((ev.target).value)}>
+ Text
+
+
- color
+ Text Color
{
- this.text.setColorAlpha(color, undefined);
- this.canvas.requestRender();
- }}
+ for (const text of this.texts) {
+ text.setColorAlpha(color, undefined);
+ }
+ this.canvas.requestRender();
+ }}
.setAlpha=${(alpha: AlphaEntry) => {
- this.text.setColorAlpha(undefined, alpha);
- this.canvas.requestRender();
- }}
+ for (const text of this.texts) {
+ 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
+
+
+
+
+
+
+
+
`;
}
diff --git a/ts/editors/EditorSelect.ts b/ts/editors/EditorSelect.ts
index c742137..a77270a 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,165 @@ 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;
+ 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);
+ }
+ }
} else {
- let current = Selection.getSelected();
- let currentType = current.type;
+ if (!event.ctrlKey) {
+ Selection.deselectAny();
+ }
+ }
- if (!currentType) { //nothing selected
+ self.dragging = false;
+ self.previewSelection = [];
+ self.canvas.requestRender();
+ 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) {
+ if (polyline.calculator.intersectsAABB(minX, minY, maxX, maxY)) {
+ self.previewSelection.push(polyline);
+ }
+ }
+
+ self.canvas.requestRender();
return true;
}
}
@@ -134,8 +204,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 +296,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 +353,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.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);
+
+ 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 +403,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
+ );
+ }
+
}
diff --git a/ts/elements/ColorAlphaElement.ts b/ts/elements/ColorAlphaElement.ts
index 2baaa88..048bc13 100644
--- a/ts/elements/ColorAlphaElement.ts
+++ b/ts/elements/ColorAlphaElement.ts
@@ -1,22 +1,60 @@
-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 {
@property()
private setColor: (color: ColorEntry) => void;
+ @property()
private setAlpha: (alpha: AlphaEntry) => void;
+ @property()
+ private currentColor: ColorEntry | undefined = undefined;
+ @property()
+ 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`
+
- ${ColorEntry.list.map(color => 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 +63,4 @@ export class ColorAlphaElement extends LitElement {
return this;
}
-}
\ No newline at end of file
+}
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/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