| @@ -109,8 +109,18 @@ function typeOfUnit(unit) { | |||||
| if (dimsEqual(unit, math.unit(1, "joules"))) { | if (dimsEqual(unit, math.unit(1, "joules"))) { | ||||
| return "energy" | return "energy" | ||||
| } | } | ||||
| return null; | |||||
| } | } | ||||
| const unitPowers = { | |||||
| "length": 1, | |||||
| "area": 2, | |||||
| "volume": 3, | |||||
| "mass": 3, | |||||
| "energy": 3 * (3 / 4) | |||||
| }; | |||||
| math.createUnit({ | math.createUnit({ | ||||
| ShoeSizeMensUS: { | ShoeSizeMensUS: { | ||||
| prefixes: "long", | prefixes: "long", | ||||
| @@ -1631,6 +1641,42 @@ function createEntityMaker(info, views, sizes, forms) { | |||||
| return maker; | 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 | // This function serializes and parses its arguments to avoid sharing | ||||
| // references to a common object. This allows for the objects to be | // references to a common object. This allows for the objects to be | ||||
| // safely mutated. | // safely mutated. | ||||
| @@ -1679,12 +1725,6 @@ function makeEntity(info, views, sizes, forms = {}) { | |||||
| view.units = {}; | view.units = {}; | ||||
| Object.entries(view.attributes).forEach(([key, val]) => { | |||||
| if (val.defaultUnit !== undefined) { | |||||
| view.units[key] = val.defaultUnit; | |||||
| } | |||||
| }); | |||||
| if ( | if ( | ||||
| config.autoMass !== "off" && | config.autoMass !== "off" && | ||||
| view.attributes.weight === undefined | 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) => { | this.sizes.forEach((size) => { | ||||
| @@ -2307,118 +2328,188 @@ function configViewOptions(entity, view) { | |||||
| holder.innerHTML = ""; | holder.innerHTML = ""; | ||||
| Object.entries(entity.views[view].attributes).forEach(([key, val]) => { | 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 { | 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; | 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 { | } 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); | |||||
| }); | }); | ||||
| } | } | ||||