overhaul: modular architecture, tests, doc generation, warning system, and UX improvements
- Modular block files: split into blocks-algo, blocks-data, blocks-proc - New modules: sections.js (labelled canvas areas), proc-browser.js (procedure tree modal), lib-loader.js (lazy library loading), templates.js (project templates), errno-data.js (errno table) - Test infrastructure: browser test suite (28 tests) and Node pure-function tests (75 tests) - Warning system: unified .block-warn for disconnected inputs, unconnected error hooks, and zero-exit-in-error-path - Error path highlighting: red flow paths and block borders for error-only subgraphs - Input hooks: hidden by default, shown during linking; outputs always at bottom - Block editor modal: pencil icon, colour picker for accent, compact field labels - Doc generation: standalone node script extracts JSDoc -> docs/API.md (124 declarations) - Persistence: save/restore sections, errno selection, block colours - Fixes: _segmentsIntersect bug, WCEnd crash from closest(this), drawpath guard on detached elements
This commit is contained in:
+233
-72
@@ -1,7 +1,7 @@
|
||||
// ── Data & Measurement tab block definitions ───────────────────────────────
|
||||
//
|
||||
// Blocks that live in the Data tab (variable area) and Measurements tab:
|
||||
// WCQuantity, WCUnit, WCBoolean, WCScalar, WCString, WCArray
|
||||
// WCQuantity, WCUnit, WCBoolean, WCScalar, WCVector, WCString, WCArray
|
||||
//
|
||||
// Each extends WCBlock (from wcblock.js) and provides generateOwnCode().
|
||||
//
|
||||
@@ -16,11 +16,12 @@
|
||||
|
||||
// ── WCQuantity — measurement quantity definition ───────────────────────────
|
||||
|
||||
/** Measurement quantity definition: name, 7 SI base dimensions, min/max range, default unit. */
|
||||
class WCQuantity extends WCBlock {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._el.name = this.querySelector(".quantity-name");
|
||||
this._el.dims = Array.from({ length: 7 }, (_, i) => this.querySelector(`.dim-${i}`));
|
||||
this._el.name = this._require(".quantity-name");
|
||||
this._el.dims = Array.from({ length: 7 }, (_, i) => this._require(`.dim-${i}`));
|
||||
this._el.min = this.querySelector(".quantity-min");
|
||||
this._el.max = this.querySelector(".quantity-max");
|
||||
this._el.defaultUnit = this.querySelector(".quantity-default-unit");
|
||||
@@ -34,9 +35,8 @@ class WCQuantity extends WCBlock {
|
||||
}
|
||||
for (const sel of this.querySelectorAll("select")) {
|
||||
sel.addEventListener("change", () => {
|
||||
for (const d of $id("variable-area").querySelectorAll("wc-scalar")) {
|
||||
d.updateSiDisplay();
|
||||
}
|
||||
const va = $id("variable-area");
|
||||
if (va) for (const d of va.querySelectorAll("wc-scalar")) d.updateSiDisplay();
|
||||
});
|
||||
}
|
||||
setTimeout(() => { refreshQuantityOptions(); refreshDefaultUnitOptions(); }, 0);
|
||||
@@ -52,25 +52,26 @@ class WCQuantity extends WCBlock {
|
||||
};
|
||||
}
|
||||
restoreData(d) {
|
||||
this.querySelector(".quantity-name").value = d.name || "";
|
||||
for (let i = 0; i < 7; i++) this.querySelector(`.dim-${i}`).value = d.dims ? d.dims[i] : "0";
|
||||
this.querySelector(".quantity-min").value = d.min || "";
|
||||
this.querySelector(".quantity-max").value = d.max || "";
|
||||
this.querySelector(".quantity-default-unit").value = d.defaultUnit || "";
|
||||
this._el.name.value = d.name || "";
|
||||
for (let i = 0; i < 7; i++) { if (this._el.dims[i]) this._el.dims[i].value = d.dims ? d.dims[i] : "0"; }
|
||||
if (this._el.min) this._el.min.value = d.min || "";
|
||||
if (this._el.max) this._el.max.value = d.max || "";
|
||||
if (this._el.defaultUnit) this._el.defaultUnit.value = d.defaultUnit || "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── WCUnit — unit of measurement ───────────────────────────────────────────
|
||||
|
||||
/** Unit of measurement: name, associated quantity, suffix, SI factor, recursive SI resolution. */
|
||||
class WCUnit extends WCBlock {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._el.fullname = this.querySelector(".unit-fullname");
|
||||
this._el.type = this.querySelector(".unit-type");
|
||||
this._el.suffix = this.querySelector(".unit-suffix");
|
||||
this._el.si = this.querySelector(".unit-si");
|
||||
this._el.resolved = this.querySelector(".unit-resolved");
|
||||
this._el.fullname = this._require(".unit-fullname");
|
||||
this._el.type = this._require(".unit-type");
|
||||
this._el.suffix = this._require(".unit-suffix");
|
||||
this._el.si = this._require(".unit-si");
|
||||
this._el.resolved = this._require(".unit-resolved");
|
||||
|
||||
for (const inp of this.querySelectorAll("input")) {
|
||||
inp.addEventListener("input", () => {
|
||||
@@ -118,10 +119,10 @@ class WCUnit extends WCBlock {
|
||||
};
|
||||
}
|
||||
restoreData(d) {
|
||||
this.querySelector(".unit-fullname").value = d.fullname || "";
|
||||
this.querySelector(".unit-type").value = d.type || "";
|
||||
this.querySelector(".unit-suffix").value = d.suffix || "";
|
||||
this.querySelector(".unit-si").value = d.si || "1";
|
||||
this._el.fullname.value = d.fullname || "";
|
||||
this._el.type.value = d.type || "";
|
||||
this._el.suffix.value = d.suffix || "";
|
||||
this._el.si.value = d.si || "1";
|
||||
}
|
||||
|
||||
updateSiDisplay() {
|
||||
@@ -135,15 +136,14 @@ class WCUnit extends WCBlock {
|
||||
|
||||
// ── WCBoolean — boolean variable ───────────────────────────────────────────
|
||||
|
||||
/** Boolean variable: name, true/false value, optional comment. Generates C `int`. */
|
||||
class WCBoolean extends WCBlock {
|
||||
get dataType() { return "boolean"; }
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._el.name = this.querySelector(".var-name");
|
||||
this._el.value = this.querySelector(".var-value");
|
||||
this._el.comment = this.querySelector(".var-comment");
|
||||
this._el.cname = this.querySelector(".var-cname");
|
||||
this._el.name = this._require(".var-name");
|
||||
this._el.value = this._require(".var-value");
|
||||
this._el.comment = this._require(".var-comment");
|
||||
this._el.cname = this._require(".var-cname");
|
||||
this._el.name.addEventListener("input", () => {
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
});
|
||||
@@ -151,9 +151,9 @@ class WCBoolean extends WCBlock {
|
||||
|
||||
saveData() { return { name: this._el.name.value, value: this._el.value.value, comment: this._el.comment.value }; }
|
||||
restoreData(d) {
|
||||
this.querySelector(".var-name").value = d.name || "";
|
||||
this.querySelector(".var-value").value = d.value || "false";
|
||||
this.querySelector(".var-comment").value = d.comment || "";
|
||||
this._el.name.value = d.name || "";
|
||||
this._el.value.value = d.value || "false";
|
||||
this._el.comment.value = d.comment || "";
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
}
|
||||
|
||||
@@ -169,18 +169,18 @@ class WCBoolean extends WCBlock {
|
||||
|
||||
// ── WCScalar — typed scalar variable with quantity, unit, and SI conversion ──
|
||||
|
||||
/** Typed scalar variable with quantity, unit, SI conversion, and auto-sync between value and unit. */
|
||||
class WCScalar extends WCBlock {
|
||||
get dataType() { return "scalar"; }
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._el.name = this.querySelector(".var-name");
|
||||
this._el.value = this.querySelector(".var-value");
|
||||
this._el.unit = this.querySelector(".var-unit");
|
||||
this._el.type = this.querySelector(".var-type");
|
||||
this._el.si = this.querySelector(".var-si");
|
||||
this._el.comment = this.querySelector(".var-comment");
|
||||
this._el.cname = this.querySelector(".var-cname");
|
||||
this._el.name = this._require(".var-name");
|
||||
this._el.value = this._require(".var-value");
|
||||
this._el.unit = this._require(".var-unit");
|
||||
this._el.type = this._require(".var-type");
|
||||
this._el.si = this._require(".var-si");
|
||||
this._el.comment = this._require(".var-comment");
|
||||
this._el.cname = this._require(".var-cname");
|
||||
|
||||
this._el.name.addEventListener("input", () => {
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
@@ -265,11 +265,11 @@ class WCScalar extends WCBlock {
|
||||
};
|
||||
}
|
||||
restoreData(d) {
|
||||
this.querySelector(".var-name").value = d.name || "";
|
||||
this.querySelector(".var-value").value = d.value || "";
|
||||
this.querySelector(".var-type").value = d.type || "";
|
||||
this.querySelector(".var-unit").value = d.unit || "";
|
||||
this.querySelector(".var-comment").value = d.comment || "";
|
||||
this._el.name.value = d.name || "";
|
||||
this._el.value.value = d.value || "";
|
||||
this._el.type.value = d.type || "";
|
||||
this._el.unit.value = d.unit || "";
|
||||
this._el.comment.value = d.comment || "";
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
}
|
||||
|
||||
@@ -285,17 +285,154 @@ class WCScalar extends WCBlock {
|
||||
}
|
||||
|
||||
|
||||
// ── WCString — string variable ─────────────────────────────────────────────
|
||||
|
||||
class WCString extends WCBlock {
|
||||
get dataType() { return "string"; }
|
||||
// ── WCVector — N-dimensional vector with quantity, unit, and SI conversion ──
|
||||
|
||||
/** N-dimensional vector variable with quantity, unit, SI conversion; generates C array. */
|
||||
class WCVector extends WCBlock {
|
||||
get dataType() { return "vector"; }
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._el.name = this.querySelector(".var-name");
|
||||
this._el.value = this.querySelector(".var-value");
|
||||
this._el.comment = this.querySelector(".var-comment");
|
||||
this._el.cname = this.querySelector(".var-cname");
|
||||
this._el.name = this._require(".var-name");
|
||||
this._el.dim = this._require(".var-dim");
|
||||
this._el.value = this._require(".var-value");
|
||||
this._el.unit = this._require(".var-unit");
|
||||
this._el.type = this._require(".var-type");
|
||||
this._el.si = this._require(".var-si");
|
||||
this._el.comment = this._require(".var-comment");
|
||||
this._el.cname = this._require(".var-cname");
|
||||
|
||||
this._el.name.addEventListener("input", () => {
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
refreshQuantityOptions();
|
||||
refreshUnitOptions();
|
||||
this.updateSiDisplay();
|
||||
}, 0);
|
||||
|
||||
this._el.value.addEventListener("input", () => {
|
||||
this.syncUnitFromValue();
|
||||
this.updateSiDisplay();
|
||||
});
|
||||
this._el.unit.addEventListener("change", () => {
|
||||
this.syncValueFromUnit();
|
||||
this.updateSiDisplay();
|
||||
});
|
||||
this._el.type.addEventListener("change", () => {
|
||||
refreshUnitOptions();
|
||||
this.updateSiDisplay();
|
||||
});
|
||||
}
|
||||
|
||||
getDim() {
|
||||
return parseInt(this._el.dim.value) || 3;
|
||||
}
|
||||
|
||||
parseValue() {
|
||||
const { numVal, unitSuffix } = splitValueAndUnit(this._el.value.value, 0);
|
||||
return { numVal, unitSuffix };
|
||||
}
|
||||
|
||||
siValue() {
|
||||
const { numVal, unitSuffix } = this.parseValue();
|
||||
if (!unitSuffix) return numVal;
|
||||
const quantityName = this._el.type.value;
|
||||
for (const u of $id("data-area").querySelectorAll("wc-unit")) {
|
||||
if (
|
||||
u.querySelector(".unit-type").value === quantityName &&
|
||||
u.querySelector(".unit-suffix").value === unitSuffix
|
||||
) {
|
||||
return numVal * u.getSiFactor();
|
||||
}
|
||||
}
|
||||
return numVal;
|
||||
}
|
||||
|
||||
syncUnitFromValue() {
|
||||
const { unitSuffix } = this.parseValue();
|
||||
if (unitSuffix && [...this._el.unit.options].some(o => o.value === unitSuffix)) {
|
||||
this._el.unit.value = unitSuffix;
|
||||
}
|
||||
}
|
||||
|
||||
syncValueFromUnit() {
|
||||
const unitSuffix = this._el.unit.value;
|
||||
const vals = this._el.value.value.trim().split(/\s+/);
|
||||
if (unitSuffix && vals.length > 0 && !isNaN(parseFloat(vals[0]))) {
|
||||
const first = parseFloat(vals[0]);
|
||||
if (vals.length <= 2) {
|
||||
this._el.value.value = first + " " + unitSuffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSiDisplay() {
|
||||
const typeName = this._el.type.value;
|
||||
const { unitSuffix } = this.parseValue();
|
||||
if (unitSuffix && typeName) {
|
||||
const si = this.siValue();
|
||||
const defaultUnit = getDefaultUnitSuffix(typeName);
|
||||
this._el.si.textContent = defaultUnit
|
||||
? `= ${si} ${defaultUnit}`
|
||||
: `= ${si} SI`;
|
||||
} else {
|
||||
this._el.si.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
saveData() {
|
||||
return {
|
||||
name: this._el.name.value,
|
||||
dim: parseInt(this._el.dim.value) || 3,
|
||||
value: this._el.value.value,
|
||||
type: this._el.type.value,
|
||||
unit: this._el.unit.value,
|
||||
comment: this._el.comment.value
|
||||
};
|
||||
}
|
||||
restoreData(d) {
|
||||
this._el.name.value = d.name || "";
|
||||
this._el.dim.value = d.dim || 3;
|
||||
this._el.value.value = d.value || "";
|
||||
this._el.type.value = d.type || "";
|
||||
this._el.unit.value = d.unit || "";
|
||||
this._el.comment.value = d.comment || "";
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
}
|
||||
|
||||
generateOwnCode() {
|
||||
const name = toCName(this._el.name.value) || "v";
|
||||
const typeName = this._el.type.value;
|
||||
const dim = this.getDim();
|
||||
const prefix = commentPrefix(this._el.name.value, this._el.comment.value);
|
||||
const baseType = typeName || "double";
|
||||
const raw = this._el.value.value.trim();
|
||||
if (raw) {
|
||||
const parts = raw.split(/\s+/).filter(s => s);
|
||||
const siFactor = this.parseValue().unitSuffix ? this.siValue() / this.parseValue().numVal : 1;
|
||||
const siParts = parts.map(p => {
|
||||
const n = parseFloat(p);
|
||||
return isNaN(n) ? 0 : n * siFactor;
|
||||
});
|
||||
return `${prefix}${baseType} ${name}[${dim}] = {${siParts.join(", ")}};\n`;
|
||||
}
|
||||
return `${prefix}${baseType} ${name}[${dim}];\n`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── WCString — string variable ─────────────────────────────────────────────
|
||||
|
||||
/** String variable: name, value. Generates C `string` struct with malloc/memcpy. */
|
||||
class WCString extends WCBlock {
|
||||
get dataType() { return "string"; }
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._el.name = this._require(".var-name");
|
||||
this._el.value = this._require(".var-value");
|
||||
this._el.comment = this._require(".var-comment");
|
||||
this._el.cname = this._require(".var-cname");
|
||||
this._el.name.addEventListener("input", () => {
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
});
|
||||
@@ -303,9 +440,9 @@ class WCString extends WCBlock {
|
||||
|
||||
saveData() { return { name: this._el.name.value, value: this._el.value.value, comment: this._el.comment.value }; }
|
||||
restoreData(d) {
|
||||
this.querySelector(".var-name").value = d.name || "";
|
||||
this.querySelector(".var-value").value = d.value || "";
|
||||
this.querySelector(".var-comment").value = d.comment || "";
|
||||
this._el.name.value = d.name || "";
|
||||
this._el.value.value = d.value || "";
|
||||
this._el.comment.value = d.comment || "";
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
}
|
||||
|
||||
@@ -322,27 +459,26 @@ class WCString extends WCBlock {
|
||||
|
||||
// ── WCArray — typed C array variable ───────────────────────────────────────
|
||||
|
||||
/** Typed C array variable with dynamic element list and optional vector dimension. */
|
||||
class WCArray extends WCBlock {
|
||||
get dataType() { return "array"; }
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._values = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._el.name = this.querySelector(".var-name");
|
||||
this._el.type = this.querySelector(".array-type");
|
||||
this._el.elements = this.querySelector(".array-elements");
|
||||
this._el.count = this.querySelector(".array-count");
|
||||
this._el.comment = this.querySelector(".var-comment");
|
||||
this._el.cname = this.querySelector(".var-cname");
|
||||
this._el.name = this._require(".var-name");
|
||||
this._el.type = this._require(".array-type");
|
||||
this._el.elements = this._require(".array-elements");
|
||||
this._el.count = this._require(".array-count");
|
||||
this._el.comment = this._require(".var-comment");
|
||||
this._el.cname = this._require(".var-cname");
|
||||
this._el.vecDim = this.querySelector(".array-vec-dim");
|
||||
|
||||
this._el.name.addEventListener("input", () => {
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
refreshVarRefOptions();
|
||||
});
|
||||
if (this._el.vecDim) {
|
||||
this._el.vecDim.addEventListener("input", () => this._updateCount());
|
||||
}
|
||||
setTimeout(() => this._renderElements(), 0);
|
||||
}
|
||||
|
||||
@@ -441,17 +577,37 @@ class WCArray extends WCBlock {
|
||||
|
||||
_addElement(value) {
|
||||
this._values.push(value);
|
||||
this._renderElements();
|
||||
const row = this._makeElementRow(this._values.length - 1);
|
||||
this._el.elements.insertBefore(row, this._el.elements.lastChild);
|
||||
const newRow = this._el.elements.lastChild;
|
||||
newRow.querySelector(".array-element-idx").textContent = this._values.length;
|
||||
this._updateCount();
|
||||
}
|
||||
|
||||
_removeElement(idx) {
|
||||
this._values.splice(idx, 1);
|
||||
this._renderElements();
|
||||
const row = this._el.elements.querySelector(`[data-idx="${idx}"]`);
|
||||
if (row) row.remove();
|
||||
const rows = this._el.elements.children;
|
||||
for (let i = 0; i < rows.length - 1; i++) {
|
||||
rows[i].dataset.idx = i;
|
||||
rows[i].querySelector(".array-element-idx").textContent = i;
|
||||
}
|
||||
this._updateCount();
|
||||
}
|
||||
|
||||
_updateCount() {
|
||||
const n = this._values.length;
|
||||
this._el.count.textContent = n + " element" + (n === 1 ? "" : "s");
|
||||
const dim = this.getVecDim();
|
||||
if (dim > 1) {
|
||||
this._el.count.textContent = n + " element" + (n === 1 ? "" : "s") + " (\u00D7" + dim + " = " + (n * dim) + ")";
|
||||
} else {
|
||||
this._el.count.textContent = n + " element" + (n === 1 ? "" : "s");
|
||||
}
|
||||
}
|
||||
|
||||
getVecDim() {
|
||||
return this._el.vecDim ? (parseInt(this._el.vecDim.value) || 1) : 1;
|
||||
}
|
||||
|
||||
saveData() {
|
||||
@@ -459,7 +615,8 @@ class WCArray extends WCBlock {
|
||||
name: this._el.name.value,
|
||||
type: this._el.type.value,
|
||||
values: [...this._values],
|
||||
comment: this._el.comment.value
|
||||
comment: this._el.comment.value,
|
||||
vecDim: this.getVecDim()
|
||||
};
|
||||
}
|
||||
restoreData(d) {
|
||||
@@ -467,6 +624,7 @@ class WCArray extends WCBlock {
|
||||
this._el.type.value = d.type || "float";
|
||||
this._el.comment.value = d.comment || "";
|
||||
this._el.cname.textContent = toCName(this._el.name.value);
|
||||
if (this._el.vecDim) this._el.vecDim.value = d.vecDim || 1;
|
||||
if (Array.isArray(d.values)) {
|
||||
this._values = [...d.values];
|
||||
} else if (typeof d.values === "string" && d.values) {
|
||||
@@ -480,11 +638,14 @@ class WCArray extends WCBlock {
|
||||
generateOwnCode() {
|
||||
const name = toCName(this._el.name.value) || "arr";
|
||||
const type = this._el.type.value || "float";
|
||||
const dim = this.getVecDim();
|
||||
const count = this._values.length || 1;
|
||||
const totalSize = dim > 1 ? dim + "*" + count : count;
|
||||
const elements = this._values.filter(s => s).join(", ");
|
||||
const prefix = commentPrefix(this._el.name.value, this._el.comment.value);
|
||||
if (elements) {
|
||||
return `${prefix}${type} ${name}[] = {${elements}};\n`;
|
||||
if (elements && dim <= 1) {
|
||||
return `${prefix}${type} ${name}[${totalSize}] = {${elements}};\n`;
|
||||
}
|
||||
return `${prefix}${type} ${name}[1];\n`;
|
||||
return `${prefix}${type} ${name}[${totalSize}];\n`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user