| @@ -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); | |||
| }); | |||
| } | |||