From b950864cba44cbe752afa7fc68fba23774cd1cfa Mon Sep 17 00:00:00 2001 From: Fen Dweller Date: Fri, 29 Apr 2022 11:05:39 -0400 Subject: [PATCH] Implement a custom attribute system --- macrovision.js | 321 +++++++++++++++++++++++++++++++------------------ 1 file changed, 206 insertions(+), 115 deletions(-) diff --git a/macrovision.js b/macrovision.js index 0748396f..b5a200ed 100644 --- a/macrovision.js +++ b/macrovision.js @@ -109,8 +109,18 @@ function typeOfUnit(unit) { if (dimsEqual(unit, math.unit(1, "joules"))) { return "energy" } + + return null; } +const unitPowers = { + "length": 1, + "area": 2, + "volume": 3, + "mass": 3, + "energy": 3 * (3 / 4) +}; + math.createUnit({ ShoeSizeMensUS: { prefixes: "long", @@ -1631,6 +1641,42 @@ function createEntityMaker(info, views, sizes, forms) { return maker; } +// Sets up the getters for each attribute. This needs to be +// re-run if we add new attributes to an entity, so it's +// broken out from makeEntity. + +function defineAttributeGetters(view) { + Object.entries(view.attributes).forEach(([key, val]) => { + + if (val.defaultUnit !== undefined) { + view.units[key] = val.defaultUnit; + } + + if (view[key] !== undefined) { + return; + } + + Object.defineProperty(view, key, { + get: function () { + return math.multiply( + Math.pow( + this.parent.scale, + this.attributes[key].power + ), + this.attributes[key].base + ); + }, + set: function (value) { + const newScale = Math.pow( + math.divide(value, this.attributes[key].base), + 1 / this.attributes[key].power + ); + this.parent.scale = newScale; + }, + }); + }); +} + // This function serializes and parses its arguments to avoid sharing // references to a common object. This allows for the objects to be // safely mutated. @@ -1679,12 +1725,6 @@ function makeEntity(info, views, sizes, forms = {}) { view.units = {}; - Object.entries(view.attributes).forEach(([key, val]) => { - if (val.defaultUnit !== undefined) { - view.units[key] = val.defaultUnit; - } - }); - if ( config.autoMass !== "off" && view.attributes.weight === undefined @@ -1789,26 +1829,7 @@ function makeEntity(info, views, sizes, forms = {}) { }; } - Object.entries(view.attributes).forEach(([key, val]) => { - Object.defineProperty(view, key, { - get: function () { - return math.multiply( - Math.pow( - this.parent.scale, - this.attributes[key].power - ), - this.attributes[key].base - ); - }, - set: function (value) { - const newScale = Math.pow( - math.divide(value, this.attributes[key].base), - 1 / this.attributes[key].power - ); - this.parent.scale = newScale; - }, - }); - }); + defineAttributeGetters(view); }); this.sizes.forEach((size) => { @@ -2307,118 +2328,188 @@ function configViewOptions(entity, view) { holder.innerHTML = ""; Object.entries(entity.views[view].attributes).forEach(([key, val]) => { - const label = document.createElement("div"); - label.classList.add("options-label"); - label.innerText = val.name; + if (val.editing) { + const name = document.createElement("input"); + name.placeholder = "Enter name..."; + holder.appendChild(name); + + holder.addEventListener("keydown", (e) => { + e.stopPropagation(); + }); - holder.appendChild(label); + const input = document.createElement("input"); + input.placeholder = "Enter measurement..."; + holder.appendChild(input); - const row = document.createElement("div"); - row.classList.add("options-row"); + input.addEventListener("keydown", (e) => { + e.stopPropagation(); + }); - holder.appendChild(row); + const button = document.createElement("button"); + button.innerText = "Confirm"; + holder.appendChild(button); - const input = document.createElement("input"); - input.classList.add("options-field-numeric"); - input.id = "options-view-" + key + "-input"; + button.addEventListener("click", e => { + let unit; + try { + unit = math.unit(input.value); + } catch { + toast("Invalid unit: " + input.value); + return; + } - const select = document.createElement("select"); - select.classList.add("options-field-unit"); - select.id = "options-view-" + key + "-select"; - - Object.entries(unitChoices[val.type]).forEach(([group, entries]) => { - const optGroup = document.createElement("optgroup"); - optGroup.label = group; - select.appendChild(optGroup); - entries.forEach((entry) => { - const option = document.createElement("option"); - option.innerText = entry; - if (entry == defaultUnits[val.type][config.units]) { - option.selected = true; + const unitType = typeOfUnit(unit); + + if (unitType === null) { + toast("Unit must be one of length, area, volume, mass, or energy."); + return; } - select.appendChild(option); + + const power = unitPowers[unitType]; + + const baseValue = math.multiply(unit, math.pow(1/entity.scale, power)); + + entity.views[view].attributes[key] = { + name: name.value, + power: power, + type: unitType, + base: baseValue, + }; + + // since we might have changed unit types, we should + // clear this. + entity.currentView.units[key] = undefined; + + defineAttributeGetters(entity.views[view]); + + configViewOptions(entity, view); }); - }); + } else { + const label = document.createElement("div"); + label.classList.add("options-label"); + label.innerText = val.name; - input.addEventListener("change", (e) => { - const raw_value = input.value == 0 ? 1 : input.value; - let value; - try { - value = math.evaluate(raw_value).toNumber(select.value); - } catch { + holder.appendChild(label); + + const row = document.createElement("div"); + row.classList.add("options-row"); + + holder.appendChild(row); + + const input = document.createElement("input"); + input.classList.add("options-field-numeric"); + input.id = "options-view-" + key + "-input"; + + const select = document.createElement("select"); + select.classList.add("options-field-unit"); + select.id = "options-view-" + key + "-select"; + + Object.entries(unitChoices[val.type]).forEach(([group, entries]) => { + const optGroup = document.createElement("optgroup"); + optGroup.label = group; + select.appendChild(optGroup); + entries.forEach((entry) => { + const option = document.createElement("option"); + option.innerText = entry; + if (entry == defaultUnits[val.type][config.units]) { + option.selected = true; + } + select.appendChild(option); + }); + }); + + input.addEventListener("change", (e) => { + const raw_value = input.value == 0 ? 1 : input.value; + let value; try { - value = math.evaluate(input.value); - if (typeof value !== "number") { - toast( - "Invalid input: " + - value.format() + - " can't convert to " + - select.value - ); + value = math.evaluate(raw_value).toNumber(select.value); + } catch { + try { + value = math.evaluate(input.value); + if (typeof value !== "number") { + toast( + "Invalid input: " + + value.format() + + " can't convert to " + + select.value + ); + value = undefined; + } + } catch { + toast("Invalid input: could not parse: " + input.value); value = undefined; } - } catch { - toast("Invalid input: could not parse: " + input.value); - value = undefined; } - } - if (value === undefined) { - return; - } - input.value = value; - entity.views[view][key] = math.unit(value, select.value); - entity.dirty = true; - if (config.autoFit) { - fitWorld(); + if (value === undefined) { + return; + } + input.value = value; + entity.views[view][key] = math.unit(value, select.value); + entity.dirty = true; + if (config.autoFit) { + fitWorld(); + } else { + updateSizes(true); + } + updateEntityOptions(entity, view); + updateViewOptions(entity, view, key); + }); + + input.addEventListener("keydown", (e) => { + e.stopPropagation(); + }); + + if (entity.currentView.units[key]) { + select.value = entity.currentView.units[key]; } else { - updateSizes(true); + entity.currentView.units[key] = select.value; } - updateEntityOptions(entity, view); - updateViewOptions(entity, view, key); - }); - input.addEventListener("keydown", (e) => { - e.stopPropagation(); - }); + select.dataset.oldUnit = select.value; - if (entity.currentView.units[key]) { - select.value = entity.currentView.units[key]; - } else { - entity.currentView.units[key] = select.value; - } + setNumericInput(input, entity.views[view][key].toNumber(select.value)); - select.dataset.oldUnit = select.value; + // TODO does this ever cause a change in the world? + select.addEventListener("input", (e) => { + const value = input.value == 0 ? 1 : input.value; + const oldUnit = select.dataset.oldUnit; + entity.views[entity.view][key] = math + .unit(value, oldUnit) + .to(select.value); + entity.dirty = true; + setNumericInput( + input, + entity.views[entity.view][key].toNumber(select.value) + ); - setNumericInput(input, entity.views[view][key].toNumber(select.value)); + select.dataset.oldUnit = select.value; + entity.views[view].units[key] = select.value; - // TODO does this ever cause a change in the world? - select.addEventListener("input", (e) => { - const value = input.value == 0 ? 1 : input.value; - const oldUnit = select.dataset.oldUnit; - entity.views[entity.view][key] = math - .unit(value, oldUnit) - .to(select.value); - entity.dirty = true; - setNumericInput( - input, - entity.views[entity.view][key].toNumber(select.value) - ); + if (config.autoFit) { + fitWorld(); + } else { + updateSizes(true); + } - select.dataset.oldUnit = select.value; - entity.views[view].units[key] = select.value; + updateEntityOptions(entity, view); + updateViewOptions(entity, view, key); + }); - if (config.autoFit) { - fitWorld(); - } else { - updateSizes(true); - } + row.appendChild(input); + row.appendChild(select); + } + }); - updateEntityOptions(entity, view); - updateViewOptions(entity, view, key); - }); + const customButton = document.createElement("button"); + customButton.innerText = "New Attribute"; + holder.appendChild(customButton); - row.appendChild(input); - row.appendChild(select); + customButton.addEventListener("click", e => { + entity.currentView.attributes["custom" + (Object.keys(entity.currentView.attributes).length + 1)] = { + name: "Custom", + editing: true + } + configViewOptions(entity, view); }); }