From ac26861cd2097171f4f61adf7277af93c7094b16 Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Sat, 19 Jan 2013 01:31:47 +0000 Subject: [PATCH] Initial version. Organization: Straylight/Edgeware From: Mark Wooding --- dep-ui.css | 8 + dep-ui.js | 206 +++++++++++++++++++++ dep.js | 512 +++++++++++++++++++++++++++++++++++++++++++++++++++ rolling.css | 24 +++ rolling.html | 188 +++++++++++++++++++ 5 files changed, 938 insertions(+) create mode 100644 dep-ui.css create mode 100644 dep-ui.js create mode 100644 dep.js create mode 100644 rolling.css create mode 100644 rolling.html diff --git a/dep-ui.css b/dep-ui.css new file mode 100644 index 0000000..7e191c4 --- /dev/null +++ b/dep-ui.css @@ -0,0 +1,8 @@ +input.bad { background-color: #ff6666; } +input[readonly] { + border: 1px inset; + background-color: #eeeeee; +} +input.bad[readonly] { + background-color: #dd5555; +} diff --git a/dep-ui.js b/dep-ui.js new file mode 100644 index 0000000..29f5d6b --- /dev/null +++ b/dep-ui.js @@ -0,0 +1,206 @@ +/* -*-javascript-*- + * + * Dependency-based user interface in a web page. + * + * (c) 2013 Mark Wooding + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +var DEP_UI = {}; (function () { with (DEP_UI) { + +/*----- Utility functions and classes -------------------------------------*/ + +DEP_UI.elt = function (id) { + /* Find and return the element with the given ID. */ + return document.getElementById(id); +} + +DEP_UI.add_elt_class = function (elt, cls) { + /* Add the class name CLS to element ELT's `class' attribute. */ + + if (!elt.className.match('\\b' + cls + '\\b')) + elt.className += ' ' + cls +} + +DEP_UI.rm_elt_class = function (elt, cls) { + /* Remove the class name CLS from element ELT's `class' attribute. */ + + elt.className = elt.className.replace( + new RegExp ('\\s*\\b' + cls + '\\b\\s*'), ' '); +} + +/* A gadget which can arrange to perform an idempotent action (the FUNC + * argument) again `soon'. + */ +DEP_UI.Soon = function (func) { + this.timer = null; + this.func = func; +} +Soon.prototype = { + kick: function () { + /* Make sure the function is called again soon. If we've already been + * kicked, then put off the action for a bit more (in case things need + * time to settle). + */ + + var me = this; + if (this.timer !== null) clearTimeout(this.timer); + this.timer = setTimeout(function () { me.func(); }, 50); + } +}; + +/*----- Conversion machinery ----------------------------------------------*/ + +/* An exception, thrown if a conversion function doesn't like what it + * sees. + */ +DEP_UI.BadValue = new DEP.Tag('BadValue'); + +DEP_UI.convert_to_numeric = function (string) { + /* Convert the argument STRING to a number. */ + + if (!string.match('\\S')) throw BadValue; + var n = Number(string); + if (n !== n) throw BadValue; + return n; +} + +DEP_UI.convert_from_numeric = function (num) { + /* Convert the argument number NUM to a string, in a useful way. */ + return num.toFixed(3); +} + +/*----- User interface functions ------------------------------------------*/ + +/* A list of input fields which might need periodic kicking. */ +var KICK_INPUT_FIELDS = []; + +DEP_UI.input_field = function (id, dep, convert) { + /* Bind an input field (with the given ID) to a DEP, converting the user's + * input with the CONVERT function. + */ + + var e = elt(id); + + function kick() { + /* Update the dep from the element content. If the convert function + * doesn't like the input then mark the dep as bad and highlight the + * input element. + */ + + var val, err; + + try { + val = convert(e.value); + if (!dep.goodp()) + rm_elt_class(e, 'bad'); + dep.set_value(val); + } catch (err) { + if (err !== BadValue) throw err; + dep.make_bad(); + add_elt_class(e, 'bad'); + } + } + + // Name the dep after our id. + dep.name = id; + + // Arrange to update the dep `shortly after' updates. + var soon = new Soon(kick); + function kick_soon () { soon.kick(); } + e.addEventListener('click', kick_soon); + e.addEventListener('blur', kick_soon); + e.addEventListener('keypress', kick_soon); + + // Sadly, the collection of events above isn't comprehensive, because we + // don't actually get told about edits as a result of clipboard operations, + // or even (sometimes) deletes, so add our `kick' function to a list of + // such functions to be run periodically just in case. + KICK_INPUT_FIELDS.push(kick); +} + +DEP_UI.input_radio = function (id, dep) { + /* Bind a radio button (with the given ID) to a DEP. When the user frobs + * the button, set the dep to the element's `value' attribute. + */ + + var e = elt(id); + + function kick () { + // Make sure we're actually chosen. We get called periodically + // regardless of user input. + if (e.checked) dep.set_value(e.value); + }; + + // Name the dep after our id. + dep.name = id; + + // Arrange to update the dep `shortly after' updates. + var soon = new Soon(kick); + function kick_soon () { soon.kick(); } + e.addEventListener('click', kick_soon); + e.addEventListener('changed', kick_soon); + + // The situation for radio buttons doesn't seem as bad as for text widgets, + // but let's be on the safe side. + KICK_INPUT_FIELDS.push(kick); +} + +DEP_UI.output_field = function (id, dep, convert) { + /* Bind a DEP to an output element (given by ID), converting the dep's + * value using the CONVERT function. + */ + + var e = elt(id); + + function kicked() { + /* Update the element, highlighting it if the dep is bad. */ + if (dep.goodp()) { + rm_elt_class(e, 'bad'); + e.value = convert(dep.value()); + } else { + add_elt_class(e, 'bad'); + e.value = ''; + } + } + + // Name the dep after our id. + dep.name = id; + + // Keep track of the dep's value. + dep.add_listener(kicked); + kicked(); +} + +/*----- Periodic maintenance ----------------------------------------------*/ + +function kick_all() { + /* Kick all of the input fields we know about. Their `kick' functions are + * all on the list `KICK_INPUT_FIELDS'. + */ + DEP.dolist(KICK_INPUT_FIELDS, function (func) { func(); }); +} + +// Update the input fields relatively frequently. +setInterval(kick_all, 500); + +// And make sure we get everything started when the page is fully loaded. +window.addEventListener('load', kick_all); + +/*----- That's all, folks -------------------------------------------------*/ +} })(); diff --git a/dep.js b/dep.js new file mode 100644 index 0000000..4376b89 --- /dev/null +++ b/dep.js @@ -0,0 +1,512 @@ +/* -*-javascript-*- + * + * Dependency-based computation. + * + * (c) 2013 Mark Wooding + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +var DEP = { }; (function () { with (DEP) { + +/*----- Utility functions and classes -------------------------------------*/ + +DEP.dolist = function (list, func) { + /* Apply FUNC to each element of LIST, discarding the results. + * + * JavaScript's looping iterates over indices rather than values, which is + * consistent with what you want from a mapping, but inconvenient for + * sequences. + */ + + var i; + for (i in list) func(list[i]); +} + +function eql(x, y) { + /* A standard equality function, suitable for detecting changes to `Dep' + * objects. This may be needlessly sensitive for some cases, but at least + * that's slow rather than wrong. + */ + + return x === y; +} + +/* A Tag is an object which is interesting only because of its identity, and + * that the set of Tags is basically determined by the static structure of + * the program. + */ +DEP.Tag = function (what) { + this.what = what; +} +Tag.prototype = { + toString: function () { return '#{Tag ' + this.what + '}'; } +} + +/* A Generation is like a Tag, except that it's expected that a program will + * manufacture Generations repeatedly, and perhaps use them to determine + * whether an object is `up-to-date' in some sense. + */ +DEP.Generation = function (what) { + this.what = what; + this.seq = Generation._next++; +} +Generation.prototype = { + toString: function () { + return '#{Generation ' + this.what + ' #' + this.seq.toString() + '}'; + } +}; +Generation._next = 0; +DEP.Generation = Generation; + +/*----- The system state --------------------------------------------------*/ + +var GENERATION = new Generation('dep-generation'); + // Current recomputation generation. + +var EVALUATING = null; // The dep being re-evaluated. +var STATE = 'ready'; // The current state. + +/* Flags for Dep objects. */ +var F_VALUE = 1; // Value is up-to-date. +var F_DEPS = 2; // Known as a dependent. +var F_CHANGED = 4; // Changed in current phase. +var F_RECOMPUTING = 8; // Currently being recomputed. +var F_QUEUED = 16; // Queued for recomputation. + +var BAD = Tag('BAD') // Used for the value of `bad' deps. + +var DELAYED = []; // Actions delayed by `with_frozen'. +var PENDING = []; // Deps awaiting recomputation. + +/*----- Exceptions --------------------------------------------------------*/ + +DEP.CircularDependency = new Tag('CircularDependency'); +DEP.BusyRecomputing = new Tag('BusyRecomputing'); + +/*----- Main code ---------------------------------------------------------*/ + +/* A `Dep' object maintains a value, either because it was provided + * explicitly by the programmer, or computed in terms of other `Dep' objects. + * In the latter case, its value will be recomputed if any of its + * dependencies change value. + * + * A `Dep' object may have listener functions attached to it. These + * functions will bw called when its value changes. + * + * A `Dep' may be `bad'. In this case, use of its value by a dependent + * causes that `Dep' to become bad in turn. + */ + +DEP.Dep = function (value, maybefunc) { + /* Initialize a new `Dep' object. + * + * Handling of the arguments is a little fiddly. If two arguments are + * provided then the first is set as the value and the second as the + * recomputation function. Otherwise, the first argument is interpreted as + * the recomputation function if it has `function' type, or as an initial + * explicit value otherwise. To force a function to be interpreted as an + * initial value, pass a second argument of `null'. + * + * Some properties can be fiddled with by client programs (rather than + * trying to invent some crazy option-parsing protocol in the constructor). + * + * `name' A string name for this `Dep', used when printing. + * + * `equivp' A predicate for deciding whether two value assigned + * to this `Dep' are equivalent. Used to decide whether + * a change should be propagated. + */ + + var val, func, f = F_CHANGED; + var me = this; + + // Work out what's going on with our crazy argument convention. + if (maybefunc !== undefined) { + val = value; + func = maybefunc; + f |= F_VALUE | F_QUEUED; + } else if (typeof value === 'function') { + val = BAD; + func = value; + f |= F_QUEUED; + } else { + val = value; + func = null; + f |= F_VALUE | F_DEPS; + } + + // Initialize the various slots. + this._value_function = func; // Recomputation function. + this._value = val; // Current value. + + this.name = undefined; // Name, for printing. + this.equivp = eql; // Equivalent-value predicate. + this.__flags = f; // Current flags. + this._generation = GENERATION; // Have we done this one already?. + this._listeners = []; // List of listener functions. + + // We need to maintain the dependency graph. In order to prevent duplicate + // entries in the various sets, we use maps rather than lists, but this + // presents a problem with coming up with keys: JavaScript stringifies + // objects rather than using them as-is, which is obviously hopeless. So + // assign each `Dep' a unique sequence number and use that as the key. + this._seq = Dep._seq++; // For identifying this object. + this._dependents = { }; // Set of dependent objects. + this._dependencies = { }; // Set of objects we depend on. + + // If we have a recomputation function then exercise it. + if (func !== null) { + with_frozen(function () { + PENDING.push(me); + }); + } +} + +Dep._seq = 0; // Next sequence number to allocate. + +Dep.prototype = { + + toString: function () { + /* Convert the receiver to a string. + * + * The printed representation includes a number of bits of information + * which are probably meaningless to most readers but are handy for + * debugging this library if it's misbehaving. + */ + + // Basic stuff. + var s = '#{Dep'; + var f = this._flags(); + var v = this._value; + + // The dep's name. + if (this.name !== null) s += ' "' + this.name + '"'; + + // Sequence number -- distinguishes the dep if nothing else will. + s += ' #' + this._seq; + + // The value, or some kind of marker that it doesn't have one. + if (!(f & F_VALUE)) s += ' #{out-of-date}'; + else if (v === BAD) s += ' #{bad}'; + else s += ' ' + v.toString(); + + // Various flags. + if (!(f & F_DEPS)) s += ' :recompute-deps'; + if (f & F_QUEUED) s += ' :queued'; + if (f & F_CHANGED) s += ' :changed'; + + // Done. + s += '}'; + return s; + }, + + _flags: function () { + /* Return the receiver's flags. + * + * If the receiver isn't up-to-date then we synthesize the flags. + */ + + if (this.state === 'ready') return F_VALUE | F_DEPS; + else if (this._generation === GENERATION) return this.__flags; + else if (this._value_function === null) return F_VALUE | F_DEPS; + else return 0; + }, + + _update: function (value) { + /* Store VALUE as the receiver's value. + * + * Return whether the value has changed as a result. This is a low-level + * function which doesn't handle propagation or anything like that. + */ + + if (value === BAD ? + this._value === BAD : + (this._value !== BAD && + this.equivp(value, this._value))) + return false; + else { + this._value = value; + return true; + } + }, + + _new_value: function () { + /* Run the `_value_function' of the receiver, returning the result. + * + * If a bad dep is read during this then return `BAD'. Other exceptions + * are propagated in the usual way. + */ + + var old = EVALUATING; + var val, e; + + this._dependencies = { }; + try { + EVALUATING = this; + try { + return this._value_function(); + } catch (e) { + if (e === BAD) return BAD; + else throw e; + } + } finally { + EVALUATING = old; + } + }, + + _propagate: function () { + /* Propagate a change in the receiver to dependents and listeners. */ + + var d, di, f; + + // Iterate over each dependent, pushing each one onto the `PENDING' + // queue, bringing it into the current generation, and marking it as + // having no current value. + for (di in this._dependents) { + d = this._dependents[di]; + f = d._flags(); + if (!(f & (F_QUEUED | F_DEPS))) { + PENDING.push(d); + d._generation = GENERATION; + d.__flags = (f & ~F_VALUE) | F_QUEUED; + } + } + + // We no longer have any dependents. Maybe we'll acquire more when the + // old dependents recompute themselves. + this._dependents = { }; + + // Tell the listeners that something interesting happened. + dolist(this._listeners, function (l) { l(); }); + }, + + _recompute: function () { + /* Recompute the receiver, propagating changes and so on. + * + * Return whether we actually needed to change anything. + */ + + var queued = this.__flags & F_QUEUED; + var e; + var me = this; + + // Update us with the given VALUE. + function update(value) { + if (me._update(value)) { + me.__flags = queued | F_VALUE | F_DEPS | F_CHANGED; + me._propagate(); + return true; + } else { + me.__flags = queued | F_VALUE | F_DEPS; + return false; + } + }; + + // Try to recompute our value. If that doesn't work then mark us as bad + // and propagate that. + try { + return update(this._new_value()); + } catch (e) { + update(BAD); + throw e; + } + }, + + _force: function () { + /* Make sure the receiver has a current value. + * + * Return true if the receiver's value has changed in this recomputation + * phase. + * + * If it already has a good value then just return. Otherwise mark it + * as recomputing (to trap circularities) and poke our dependencies to + * ensure that they're up-to-date. If they weren't, then we recompute + * our own value before returning. + */ + + var f = this._flags(); + var d, any = false; + + if (f & F_RECOMPUTING) throw CircularDependency; + else if (f & F_VALUE) return f & F_CHANGED; + else { + this._generation = GENERATION; + this.__flags = (f & ~F_QUEUED) | F_RECOMPUTING; + for (d in this._dependencies) + if (this._dependencies[d]._force()) any = true; + if (any) + return this._recompute(); + else { + this.__flags = f; + return false; + } + } + }, + + value: function () { + /* Fetch and return the receiver's current value. + * + * If the receiver is bad then an exception is thrown. This exception + * can be caught using `orelse'; a dep recomputation function can let the + * exception propagate, and be marked as bad in turn. + */ + + var val; + + if (state === 'recomputing') { + if (EVALUATING) { + this._dependents[EVALUATING._seq] = EVALUATING; + EVALUATING._dependencies[this._seq] = this; + } + this._force(); + } + val = this._value; + if (val === BAD) throw BAD; + return val; + }, + + set_value: function (value) { + /* Set the receiver's value to VALUE, and propagate. */ + + var me = this; + + with_frozen(function () { + if (me._update(value)) { + me._generation = GENERATION; + me.__flags = F_VALUE | F_CHANGED; + me._propagate(); + } + }); + }, + + make_bad: function () { + /* Mark the receiver as being bad, and propagate. */ + this.set_value(BAD); + }, + + add_listener: function (func) { + /* Add FUNC to the receiver's list of listeners. + * + * Listener functions are called without arguments, and any values + * returned are ignored. + */ + + this._listeners.push(func); + }, + + goodp: function () { + /* Return whether the receiver is good (i.e., not marked as bad). */ + + return this._value !== BAD; + } +}; + +DEP.orelse = function (thunk, errthunk) { + /* Call THUNK. If it succeeds, then return its result. If THUNK + * reads a bad dep then call ERRTHINK and return its result instead. + */ + + var e; + try { return thunk(); } + catch (e) { + if (e === BAD) { return errthunk(); } + else throw e; + } +} + +DEP.bad = function () { + /* For use in a value-function: make the dep be bad. */ + throw BAD; +} + +function recompute_pending() { + /* Recompute any pending dependencies. + * + * The state is `recomputing' during this. + */ + + var d, f; + + state = 'recomputing'; + try { + while (PENDING.length) { + d = PENDING.shift(); + f = d.__flags; + d.__flags = f & ~F_QUEUED; + if (!(f & F_VALUE)) + d._recompute(); + else if (!(f & F_DEPS)) { + d.new_value(); + d.__flags = f | F_DEPS; + } + } + } finally { + while (PENDING.length) { + d = PENDING.shift(); + d._value = BAD; + } + } +} + +DEP.with_frozen = function (body, delay) { + /* Call BODY with dependencies frozen. + * + * If the BODY function changes any dep values (using D.set_value(...)) + * then dependents won't be updated until the BODY completes. + * + * It's very + * bad to do this during a recomputation phase. If DELAY is true, then the + * BODY is delayed until the recomputation completes; otherwise you get an + * exception. + */ + + var op, val; + var old_delayed, old_pending; + + switch (STATE) { + case 'frozen': + body(); + break; + case 'recomputing': + if (!delay) throw BusyRecomputing; + DELAYED.push(body); + break; + case 'ready': + old_delayed = DELAYED; + old_pending = PENDING; + try { + DELAYED = []; + PENDING = []; + GENERATION = new Generation('dep-generation'); + val = body(); + for (;;) { + recompute_pending(); + if (!DELAYED.length) break; + op = DELAYED.shift(); + op(); + } + } finally { + DELAYED = old_delayed; + PENDING = old_pending; + } + break; + } +} + +/*----- That's all, folks -------------------------------------------------*/ +} })(); diff --git a/rolling.css b/rolling.css new file mode 100644 index 0000000..a1bbca5 --- /dev/null +++ b/rolling.css @@ -0,0 +1,24 @@ +td.label { text-align: right; } +.conceal { display: none; } +h1 { + padding-bottom: 1ex; + border-bottom-style: solid; + border-width: medium; +} +h2 { + padding-top: 1ex; + margin-top: 2ex; +} +h1 + h2, h2:first-child { + border-top-style: hidden; + margin-top: 2ex; +} +.widgets { + float: left; + width: auto; + margin-right: 2em; + margin-bottom: 2em; +} +#toggle-help { + text-align: right; +} diff --git a/rolling.html b/rolling.html new file mode 100644 index 0000000..fbee44e --- /dev/null +++ b/rolling.html @@ -0,0 +1,188 @@ + + + + Rolling wire-strip calculator + + + + + + + +

Rolling wire-strip calculator

+ + + + + + + + + + + + +

Required size

+
+ + +
+ + +
+ + + +

You should start with

+
+ + +
+ + +
+ + + +

Initial stock

+
+ + + +
+ + + +
+ + +
+ + +
+ + +
+ +

What this program does

+ +
+ +

This page is only really interesting because it contains a Javascript +program. You seem to have Javascript turned off, so it won't work very +well. +

+ +

Background

+ +

When you pass round or square wire through flat rolls it gets flatter +and longer, but it also gets wider. You can exploit this to make thin +wire strip of almost any dimensions, but it’s rather difficult to +predict the result. That’s what this calculator does. + +

You specify the width and thickness of the strip you want, and the +program calculates what size round or square wire you should start with. +Additionally, if you specify the length of strip you need, it will +calculate the length of the input wire. + +

The chances are that you don’t actually have the required +thickness of round or square wire to start with, but you do have some +that’s thicker. Just enter the size of the round or square +initial stock that you do have and the program will calculate how much +of it you need to create the required starting size that you can then +roll down to the required thickness of strip. + +

For best results, roll the strip in as few passes as you can handle. + +

Use

+ +

Boxes with light red or white backgrounds are entry boxes for you to +type in your requirements; boxes with dark red or grey backgrounds are +where the calculator puts its answers. White and grey mean that the box +is showing useful information: an input box contains a valid number, for +example, and an output box has calculated a correct answer. Red, on the +other hand, means that there’s something wrong: either the +calculator can’t understand what you’ve typed in an input +box, or it’s hit trouble – usually this means that some +necessary input is missing. + +

+ + -- [mdw]