diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76b4c55f..2510c059 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,7 +146,7 @@ By participating, you can help improve the project and make it even better :rais As the remote branch is already linked -8. **Rebase Onto Current Main:** Rebase your feature branch onto the current main branch of the original repo. +8. **Rebase Onto Current Main:** Rebase your feature branch onto the current main branch of the original repo. This will include any changes that might have been pushed into the main in the meantime and resolve possible conflicts. To sync your fork with the original upstream repo, check out [this page](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) or follow the steps below. Note that before you can sync your fork with an upstream repo, you must configure a remote that points to the upstream repository in Git. diff --git a/canvas_editor/editor/templates/editor/editor.html b/canvas_editor/editor/templates/editor/editor.html index 564ec033..132a95f8 100644 --- a/canvas_editor/editor/templates/editor/editor.html +++ b/canvas_editor/editor/templates/editor/editor.html @@ -13,6 +13,7 @@ "three/examples/jsm/": "https://cdn.jsdelivr.net/npm/three@0.178.0/examples/jsm/", "bootstrap": "https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.esm.min.js", "@popperjs/core": "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/esm/popper.min.js", + "lit": "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js", "navbar": "{% static 'js/editor/navbar.mjs' %}", "compass": "{% static 'js/editor/compass.mjs' %}", "objects": "{% static 'js/editor/objects.mjs' %}", @@ -28,6 +29,7 @@ "saveAndLoadHandler": "{% static 'js/editor/saveAndLoadHandler.mjs' %}", "bulkObjectCommands": "{% static 'js/commands/bulkObjectCommands.mjs' %}", "overview": "{% static 'js/editor/overview.mjs' %}", + "object-overview": "{% static 'js/editor/sidebar/objectOverview.mjs' %}", "editor": "{% static 'js/editor/editor.mjs' %}", "objectManager": "{% static 'js/editor/objectManager.mjs' %}", "quickSelector": "{% static 'js/editor/quickSelector.mjs' %}", diff --git a/canvas_editor/editor/templates/editor/sidebar/itemOverview.html b/canvas_editor/editor/templates/editor/sidebar/itemOverview.html index 66fb1252..eb14f551 100644 --- a/canvas_editor/editor/templates/editor/sidebar/itemOverview.html +++ b/canvas_editor/editor/templates/editor/sidebar/itemOverview.html @@ -1,47 +1,7 @@ -
-
-
-

- -

-
-
-
-
-
-

- -

-
-
-
-
-
-

- -

-
-
-
-
-
-
+
diff --git a/canvas_editor/jsconfig.json b/canvas_editor/jsconfig.json index 55be9334..7ed4941b 100644 --- a/canvas_editor/jsconfig.json +++ b/canvas_editor/jsconfig.json @@ -19,6 +19,7 @@ "saveAndLoadHandler": ["static/js/editor/saveAndLoadHandler.mjs"], "bulkObjectCommands": ["static/js/commands/bulkObjectCommands.mjs"], "overview": ["static/js/editor/overview.mjs"], + "object-overview": ["static/js/editor/sidebar/objectOverview.mjs"], "editor": ["static/js/editor/editor.mjs"], "objectManager": ["static/js/editor/objectManager.mjs"], "quickSelector": ["static/js/editor/quickSelector.mjs"], diff --git a/canvas_editor/package-lock.json b/canvas_editor/package-lock.json index 391aef25..14b99e85 100644 --- a/canvas_editor/package-lock.json +++ b/canvas_editor/package-lock.json @@ -1,13 +1,13 @@ { "name": "canvas_editor", - "version": "1.0.0", + "version": "1.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "canvas_editor", - "version": "1.0.0", - "license": "ISC", + "version": "1.0-beta", + "license": "MIT", "devDependencies": { "@eslint/js": "^9.33.0", "@types/bootstrap": "^5.2.10", @@ -15,7 +15,8 @@ "eslint": "^9.35.0", "eslint-plugin-jsdoc": "^54.0.0", "globals": "^15.15.0", - "lint-staged": "^16.1.2" + "lint-staged": "^16.1.2", + "lit": "^3.3.1" } }, "node_modules/@dimforge/rapier3d-compat": { @@ -262,6 +263,23 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", + "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz", + "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.4.0" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -327,6 +345,13 @@ "meshoptimizer": "~0.18.1" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/webxr": { "version": "0.5.22", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz", @@ -361,6 +386,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -681,6 +707,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1233,6 +1260,40 @@ "node": ">=20.0.0" } }, + "node_modules/lit": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz", + "integrity": "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz", + "integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.4.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz", + "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", diff --git a/canvas_editor/package.json b/canvas_editor/package.json index 651808dd..a5fb351b 100644 --- a/canvas_editor/package.json +++ b/canvas_editor/package.json @@ -13,7 +13,8 @@ "eslint": "^9.35.0", "eslint-plugin-jsdoc": "^54.0.0", "globals": "^15.15.0", - "lint-staged": "^16.1.2" + "lint-staged": "^16.1.2", + "lit": "^3.3.1" }, "lint-staged": { "*.{js,mjs}": "npx eslint --config canvas_editor/eslint.config.mjs" diff --git a/canvas_editor/project_management/migrations/0002_alter_lightsource_distribution_type_and_more.py b/canvas_editor/project_management/migrations/0002_alter_lightsource_distribution_type_and_more.py new file mode 100644 index 00000000..f5005165 --- /dev/null +++ b/canvas_editor/project_management/migrations/0002_alter_lightsource_distribution_type_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.7 on 2025-11-06 09:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + """Updated the possible length of type fields for the objects.""" + + dependencies = [ + ("project_management", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="lightsource", + name="distribution_type", + field=models.CharField(default="normal", max_length=50), + ), + migrations.AlterField( + model_name="lightsource", + name="light_source_type", + field=models.CharField(default="sun", max_length=50), + ), + migrations.AlterField( + model_name="receiver", + name="receiver_type", + field=models.CharField(default="planar", max_length=50), + ), + ] diff --git a/canvas_editor/pyproject.toml b/canvas_editor/pyproject.toml index 0c791def..bc22d314 100644 --- a/canvas_editor/pyproject.toml +++ b/canvas_editor/pyproject.toml @@ -91,4 +91,3 @@ convention = "numpy" [tool.djlint] profile = "django" - diff --git a/canvas_editor/static/js/editor/editor.mjs b/canvas_editor/static/js/editor/editor.mjs index e7a4c18b..31bfd7bc 100644 --- a/canvas_editor/static/js/editor/editor.mjs +++ b/canvas_editor/static/js/editor/editor.mjs @@ -3,7 +3,6 @@ import { ViewHelper } from "compass"; import { UndoRedoHandler } from "undoRedoHandler"; import { SaveAndLoadHandler } from "saveAndLoadHandler"; import { Navbar } from "navbar"; -import { OverviewHandler } from "overview"; import { Picker } from "picker"; import { ProjectSettingsManager } from "projectSettingsManager"; import { ObjectManager } from "objectManager"; @@ -28,6 +27,7 @@ import { skyboxPyPath, skyboxPzPath, } from "path_dict"; +import { Overview, OverviewController } from "object-overview"; /** * Represents the main editor class. @@ -41,7 +41,7 @@ export class Editor { #saveAndLoadHandler; #navbar; // eslint-disable-line no-unused-private-class-members -- for structural consistency, not used yet #picker; - #overview; // eslint-disable-line no-unused-private-class-members -- for structural consistency, not used yet + #overview; #modeSelector; // eslint-disable-line no-unused-private-class-members -- for structural consistency, not used yet #projectSettingManager; #objectManager; @@ -98,7 +98,11 @@ export class Editor { this.#selectionBox, this.#selectableGroup, ); - this.#overview = new OverviewHandler(this.#picker); + + this.#overview = new Overview(this.#picker); + document.getElementById("overview-tab-pane").append(this.#overview); + new OverviewController(this.#overview); + this.#projectSettingManager = new ProjectSettingsManager(); this.#projectSettingManager.initialize(); this.#objectManager = new ObjectManager( diff --git a/canvas_editor/static/js/editor/overview.mjs b/canvas_editor/static/js/editor/overview.mjs deleted file mode 100644 index 9494e0e7..00000000 --- a/canvas_editor/static/js/editor/overview.mjs +++ /dev/null @@ -1,397 +0,0 @@ -import { CanvasObject } from "canvasObject"; -import { Editor } from "editor"; -import { Heliostat } from "heliostat"; -import { LightSource } from "lightSource"; -import { Picker } from "picker"; -import { Receiver } from "receiver"; - -/** - * Class to manage the overview panel in the editor. - */ -export class OverviewHandler { - #editor; - #picker; - #overviewButton; - /** - * @type {CanvasObject[]} - */ - #selectedObjects = []; - #heliostatList; - #receiverList; - #lightsourceList; - #htmlToObject = new Map(); - #objectToHtml = new Map(); - - #objectType = Object.freeze({ - HELIOSTAT: "heliostat", - RECEIVER: "receiver", - LIGHTSOURCE: "light source", - }); - - /** - * Creates a new overview handler. - * @param {Picker} picker the picker currently in use. - */ - constructor(picker) { - this.#picker = picker; - this.#editor = Editor.getInstance(); - this.#overviewButton = document.getElementById("overview-tab"); - this.#heliostatList = document.getElementById("heliostatList"); - this.#receiverList = document.getElementById("receiverList"); - this.#lightsourceList = document.getElementById("lightsourceList"); - - // render when overview tab is selected - this.#overviewButton.addEventListener("click", () => { - this.#render(); - }); - - // re-render when a new item is selected - document.getElementById("canvas").addEventListener("itemSelected", () => { - if (this.#overviewButton.classList.contains("active")) { - this.#render(); - } - }); - - // re-render when object is created - document.getElementById("canvas").addEventListener("itemCreated", () => { - if (this.#overviewButton.classList.contains("active")) { - this.#render(); - } - }); - - // re-render when object is deleted - document.getElementById("canvas").addEventListener("itemDeleted", () => { - if (this.#overviewButton.classList.contains("active")) { - this.#render(); - } - }); - - // re-render when object is updated - document.getElementById("canvas").addEventListener("itemUpdated", () => { - if (this.#overviewButton.classList.contains("active")) { - this.#render(); - } - }); - - // handle F2 to rename - document.addEventListener("keyup", (event) => { - if ( - event.key == "F2" && - this.#overviewButton.classList.contains("active") - ) { - if (this.#selectedObjects.length !== 1) { - alert("Exactly one object must selected to rename it"); - } else { - const object = this.#selectedObjects[0]; - const type = this.#objectToHtml.get(object).dataset.type; - this.#openEditInput(this.#selectedObjects[0], type); - } - } - }); - this.#handleUserInput(); - } - - /** - * Renders the overview panel. - */ - #render() { - // clear the list - this.#heliostatList.innerText = ""; - this.#receiverList.innerText = ""; - this.#lightsourceList.innerText = ""; - - const objects = this.#editor.objects; - const selectedObjects = this.#picker.getSelectedObjects(); - - // render the objects - objects.heliostatList.forEach((heliostat) => { - const selected = selectedObjects.includes(heliostat); - this.#heliostatList.appendChild( - this.#createHeliostatEntry(heliostat, selected), - ); - }); - - objects.receiverList.forEach((receiver) => { - const selected = selectedObjects.includes(receiver); - this.#receiverList.appendChild( - this.#createReceiverEntry(receiver, selected), - ); - }); - - objects.lightsourceList.forEach((lightsource) => { - const selected = selectedObjects.includes(lightsource); - this.#lightsourceList.appendChild( - this.#createLightsourceEntry(lightsource, selected), - ); - }); - - if (this.#heliostatList.children.length == 0) { - const text = document.createElement("i"); - text.classList.add("text-secondary"); - text.innerText = "No heliostats in this scene"; - this.#heliostatList.appendChild(text); - } - - if (this.#receiverList.children.length == 0) { - const text = document.createElement("i"); - text.classList.add("text-secondary"); - text.innerText = "No receivers in this scene"; - this.#receiverList.appendChild(text); - } - - if (this.#lightsourceList.children.length == 0) { - const text = document.createElement("i"); - text.classList.add("text-secondary"); - text.innerText = "No light sources in this scene"; - this.#lightsourceList.appendChild(text); - } - } - - /** - * Creates an entry for the given heliostat - * @param {Heliostat} object the heliostat you want to create an entry for - * @param {boolean} selected if the object is selected or not - * @returns {HTMLElement} heliostatEntry - the html element for the heliostat - */ - #createHeliostatEntry(object, selected) { - // create the html element to render - const heliostatEntry = document.createElement("div"); - heliostatEntry.role = "button"; - heliostatEntry.classList.add( - "d-flex", - "gap-2", - "p-2", - "rounded-2", - "overviewElem", - selected ? "bg-primary-subtle" : "bg-body-secondary", - ); - - const icon = document.createElement("i"); - icon.classList.add( - "bi-arrow-up-right-square", - "d-flex", - "align-items-center", - ); - heliostatEntry.appendChild(icon); - - const text = document.createElement("div"); - text.classList.add("w-100", "d-flex", "align-items-center"); - text.style.whiteSpace = "normal"; - text.style.wordBreak = "break-word"; - text.innerText = object.objectName !== "" ? object.objectName : "Heliostat"; - heliostatEntry.appendChild(text); - - const button = document.createElement("button"); - button.classList.add("btn", "btn-primary", "custom-btn"); - button.style.height = "38px"; - button.style.flexShrink = "0"; - button.style.alignSelf = "center"; - const buttonIcon = document.createElement("i"); - buttonIcon.classList.add("bi", "bi-pencil-square"); - button.appendChild(buttonIcon); - heliostatEntry.appendChild(button); - - this.#addEditFunctionality(button, object, this.#objectType.HELIOSTAT); - - heliostatEntry.dataset.apiId = object.apiID.toString(); - heliostatEntry.dataset.type = this.#objectType.HELIOSTAT; - - this.#htmlToObject.set(heliostatEntry, object); - this.#objectToHtml.set(object, heliostatEntry); - return heliostatEntry; - } - - /** - * Creates an entry for the given receiver - * @param {Receiver} object the receiver you want to create an entry for - * @param {boolean} selected determines if the object is selected or not - * @returns {HTMLElement} receiverEntry - the html element for the receiver - */ - #createReceiverEntry(object, selected) { - // create the html element to render - const receiverEntry = document.createElement("div"); - receiverEntry.role = "button"; - receiverEntry.classList.add( - "d-flex", - "gap-2", - "p-2", - "rounded-2", - "overviewElem", - selected ? "bg-primary-subtle" : "bg-body-secondary", - ); - - const icon = document.createElement("i"); - icon.classList.add("bi", "bi-align-bottom", "d-flex", "align-items-center"); - receiverEntry.appendChild(icon); - - const text = document.createElement("div"); - text.classList.add("w-100", "d-flex", "align-items-center"); - text.style.whiteSpace = "normal"; - text.style.wordBreak = "break-word"; - text.innerText = - object.objectName !== "" && object.objectName - ? object.objectName - : "Receiver"; - receiverEntry.appendChild(text); - - const button = document.createElement("button"); - button.classList.add("btn", "btn-primary", "custom-btn"); - button.style.height = "38px"; - button.style.flexShrink = "0"; - button.style.alignSelf = "center"; - const buttonIcon = document.createElement("i"); - buttonIcon.classList.add("bi", "bi-pencil-square"); - button.appendChild(buttonIcon); - receiverEntry.appendChild(button); - - this.#addEditFunctionality(button, object, this.#objectType.RECEIVER); - - receiverEntry.dataset.apiId = object.apiID.toString(); - receiverEntry.dataset.type = this.#objectType.RECEIVER; - - this.#htmlToObject.set(receiverEntry, object); - this.#objectToHtml.set(object, receiverEntry); - return receiverEntry; - } - - /** - * Creates an entry for the given light source - * @param {LightSource} object the light source you want to create an entry for - * @param {boolean} selected determines if the object is selected or not - * @returns {HTMLElement} lightSourceEntry - the html element for the light source - */ - #createLightsourceEntry(object, selected) { - // create the html element to render - const lightsourceEntry = document.createElement("div"); - lightsourceEntry.role = "button"; - lightsourceEntry.classList.add( - "d-flex", - "gap-2", - "p-2", - "rounded-2", - "overviewElem", - selected ? "bg-primary-subtle" : "bg-body-secondary", - ); - - const icon = document.createElement("i"); - icon.classList.add("bi", "bi-lightbulb", "d-flex", "align-items-center"); - lightsourceEntry.appendChild(icon); - - const text = document.createElement("div"); - text.classList.add("w-100", "d-flex", "align-items-center"); - text.style.whiteSpace = "normal"; - text.style.wordBreak = "break-word"; - text.innerText = - object.objectName !== "" && object.objectName - ? object.objectName - : "Light source"; - lightsourceEntry.appendChild(text); - - const button = document.createElement("button"); - button.classList.add("btn", "btn-primary", "custom-btn"); - button.style.height = "38px"; - button.style.flexShrink = "0"; - button.style.alignSelf = "center"; - const buttonIcon = document.createElement("i"); - buttonIcon.classList.add("bi", "bi-pencil-square"); - button.appendChild(buttonIcon); - lightsourceEntry.appendChild(button); - - this.#addEditFunctionality(button, object, this.#objectType.LIGHTSOURCE); - - lightsourceEntry.dataset.apiId = object.apiID.toString(); - lightsourceEntry.dataset.type = this.#objectType.LIGHTSOURCE; - - this.#htmlToObject.set(lightsourceEntry, object); - this.#objectToHtml.set(object, lightsourceEntry); - return lightsourceEntry; - } - - /** - * Handles user input in the overview panel. - */ - #handleUserInput() { - document - .getElementById("accordionOverview") - .addEventListener("click", (event) => { - const target = event.target.closest(".overviewElem"); - const object = this.#htmlToObject.get(target); - - if (target && object) { - if (event.ctrlKey) { - if (this.#selectedObjects.includes(object)) { - this.#selectedObjects.splice( - this.#selectedObjects.indexOf(object), - 1, - ); - } else { - this.#selectedObjects.push(object); - } - } else { - this.#selectedObjects = [object]; - } - - this.#picker.setSelection(this.#selectedObjects); - } - }); - } - - /** - * Adds edit functionality to the given button. - * @param {HTMLButtonElement} button the button to open the edit field. - * @param {CanvasObject} object the object you want to edit. - * @param {"heliostat" | "receiver" | "light source"} type the type of object you want to edit the name of. - */ - #addEditFunctionality(button, object, type) { - button.addEventListener("click", (event) => { - event.stopPropagation(); - this.#openEditInput(object, type); - }); - } - - /** - * Opens a new edit field for the given object - * @param {CanvasObject} object the object you want rename. - * @param {"heliostat" | "receiver" | "light source"} type the type of object you want to edit the name of. - */ - #openEditInput(object, type) { - const entry = this.#objectToHtml.get(object); - const inputField = document.createElement("input"); - inputField.type = "text"; - inputField.classList.add("form-control", "rounded-1"); - inputField.value = - object.objectName != "" && object.objectName - ? object.objectName - : type.charAt(0).toUpperCase() + type.slice(1, type.length); - entry.innerText = ""; - entry.appendChild(inputField); - inputField.focus(); - inputField.select(); - - inputField.addEventListener("click", (event) => { - event.stopPropagation(); - }); - - inputField.addEventListener("keyup", (event) => { - if (event.key == "Escape") { - inputField.value = object.objectName; - this.#render(); - } else if (event.key == "Enter") { - this.#render(); - } - }); - - inputField.addEventListener("change", () => { - if ( - inputField.value !== object.objectName && - inputField.value.length < 200 - ) { - object.updateAndSaveObjectName(inputField.value); - } - }); - - inputField.addEventListener("blur", () => { - this.#render(); - }); - } -} diff --git a/canvas_editor/static/js/editor/sidebar/objectOverview.mjs b/canvas_editor/static/js/editor/sidebar/objectOverview.mjs new file mode 100644 index 00000000..4ae16d3b --- /dev/null +++ b/canvas_editor/static/js/editor/sidebar/objectOverview.mjs @@ -0,0 +1,297 @@ +import { CanvasObject } from "canvasObject"; +import { Editor } from "editor"; +import { html, LitElement } from "lit"; +import { Picker } from "picker"; + +/** + * Represents a single entry of the object overview. + * The user can use this entry to select or rename the object. + */ +export class OverviewEntry extends LitElement { + static properties = { + _editing: { type: Boolean }, + _selected: { type: Boolean }, + }; + #object; + #icon; + #category; + #onClick; + + /** + * Creates a new entry element + * @param {string} category the category of this overview entry + * @param {CanvasObject} object the object this entry represents + * @param {string} icon the bootstrap icon name for this entry + * @param {(e: MouseEvent) => void} onClick callback that gets executed when clicking the entry + */ + constructor(category, object, icon, onClick) { + super(); + this.#object = object; + this.#icon = icon; + this.#category = category; + this.#onClick = onClick; + this._editing = false; + this._selected = false; + } + + /** + * @inheritdoc + */ + createRenderRoot() { + return this; // Renders directly into the Light DOM + } + + /** + * @inheritdoc + * @param {Map} changedProperties Map containing all the changes properties and the old values + */ + updated(changedProperties) { + if (changedProperties.has("_editing") && this._editing) { + // Find the input element and focus it when _editing becomes true + const input = document.getElementById( + `${this.#category + "-" + this.#object.id}-overview-input`, + ); + if (input instanceof HTMLInputElement) { + input.focus(); + input.select(); + } + } + } + + /** + * @inheritdoc + */ + render() { + return this._editing + ? html`
+ +
` + : html`
+ +

+ ${this.#object.objectName} +

+ +
`; + } +} +customElements.define("overview-entry", OverviewEntry); + +/** + * @typedef {import("lit").ReactiveController} ReactiveController + */ + +/** + * @implements {ReactiveController} + * Controller for the actual overview + */ +export class OverviewController { + #overview; + #overviewButton = document.getElementById("overview-tab"); + #canvas = document.getElementById("canvas"); + static #events = [ + "itemSelected", + "itemCreated", + "itemDeleted", + "itemUpdated", + ]; + + /** + * Controller to automatically update the overview. + * @param {Overview} overview the overview view element this controller should control. + */ + constructor(overview) { + this.#overview = overview; + this.#overview.addController(this); + this.updateOverview = this.updateOverview.bind(this); + } + + /** + * @inheritdoc + */ + updateOverview = () => { + if (this.#overviewButton.classList.contains("active")) { + this.#overview.requestUpdate(); + } + }; + + /** + * @inheritdoc + */ + hostConnected() { + this.#overviewButton.addEventListener("click", this.updateOverview); + for (const event of OverviewController.#events) { + this.#canvas.addEventListener(event, this.updateOverview); + } + } + + /** + * @inheritdoc + */ + hostDisconnected() { + this.#overviewButton.removeEventListener("click", this.updateOverview); + for (const event of OverviewController.#events) { + this.#canvas.removeEventListener(event, this.updateOverview); + } + } +} + +/** + * Overview sidebar component for displaying and managing + * heliostats, receivers, and light sources in an accordion UI. + */ +export class Overview extends LitElement { + static properties = { + _selectedElements: { type: OverviewEntry }, + }; + #editor = Editor.getInstance(); + #picker; + + /** + * Creates a new Overview element. + * @param {Picker} picker the picker currently in use. + */ + constructor(picker) { + super(); + this.#picker = picker; + } + + /** + * @inheritdoc + */ + createRenderRoot() { + return this; // Renders directly into the Light DOM + } + + /** + * Creates a bootstrap accordion item + * @param {string} name the name of the accordion item. + * @param {OverviewEntry[]} items the items placed inside the accordion item + * @returns {import("lit").TemplateResult} the accordion item + */ + #createAccordionItem(name, items) { + return html` +
+

+ +

+
+
${items}
+
+
+ `; + } + + /** + * Updates the currently selected objects + * @param {CanvasObject} item the item you want to select or add to the selection + * @param {MouseEvent} event determines whether to add the item to selection or to select it + */ + #updateSelection(item, event) { + if (event.ctrlKey) { + this.#picker.setSelection([...this.#picker.getSelectedObjects(), item]); + } else { + this.#picker.setSelection([item]); + } + } + + /** + * @inheritdoc + */ + render() { + return html` +
+ ${this.#createAccordionItem( + "Heliostats", + this.#editor.objects.heliostatList.map((heliostat) => { + const tmp = new OverviewEntry( + "heliostat", + heliostat, + "bi-pencil-square", + (e) => this.#updateSelection(heliostat, e), + ); + tmp._selected = this.#picker + .getSelectedObjects() + .includes(heliostat); + return tmp; + }), + )} + ${this.#createAccordionItem( + "Receivers", + this.#editor.objects.receiverList.map((receiver) => { + const tmp = new OverviewEntry( + "receiver", + receiver, + "bi-pencil-square", + (e) => this.#updateSelection(receiver, e), + ); + tmp._selected = this.#picker + .getSelectedObjects() + .includes(receiver); + return tmp; + }), + )} + ${this.#createAccordionItem( + "Light sources", + this.#editor.objects.lightsourceList.map((lightSource) => { + const tmp = new OverviewEntry( + "lightSource", + lightSource, + "bi-pencil-square", + (e) => this.#updateSelection(lightSource, e), + ); + tmp._selected = this.#picker + .getSelectedObjects() + .includes(lightSource); + return tmp; + }), + )} +
+ `; + } +} +customElements.define("object-overview", Overview);