Files
avcf/blocks-data.js
T
adrien b8e8ef9113 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
2026-06-22 18:31:23 +02:00

652 lines
21 KiB
JavaScript

// ── Data & Measurement tab block definitions ───────────────────────────────
//
// Blocks that live in the Data tab (variable area) and Measurements tab:
// WCQuantity, WCUnit, WCBoolean, WCScalar, WCVector, WCString, WCArray
//
// Each extends WCBlock (from wcblock.js) and provides generateOwnCode().
//
// Load order dependencies:
// wcblock.js → WCBlock base class
// helpers.js → splitValueAndUnit, rebuildSelect, refreshQuantityOptions,
// refreshUnitOptions, refreshDefaultUnitOptions, refreshVarRefOptions,
// getDefaultUnitSuffix, toCName, commentPrefix, escapeString,
// refreshAll
// dom.js → $id
// ── 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._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");
for (const inp of this.querySelectorAll("input")) {
inp.addEventListener("input", () => {
refreshQuantityOptions();
refreshDefaultUnitOptions();
refreshUnitOptions();
});
}
for (const sel of this.querySelectorAll("select")) {
sel.addEventListener("change", () => {
const va = $id("variable-area");
if (va) for (const d of va.querySelectorAll("wc-scalar")) d.updateSiDisplay();
});
}
setTimeout(() => { refreshQuantityOptions(); refreshDefaultUnitOptions(); }, 0);
}
saveData() {
return {
name: this._el.name.value,
dims: this._el.dims.map(el => el.value),
min: this._el.min.value,
max: this._el.max.value,
defaultUnit: this._el.defaultUnit.value
};
}
restoreData(d) {
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._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", () => {
refreshUnitOptions();
refreshDefaultUnitOptions();
this.updateSiDisplay();
});
}
for (const sel of this.querySelectorAll("select")) {
sel.addEventListener("change", () => {
refreshUnitOptions();
refreshDefaultUnitOptions();
});
}
setTimeout(() => {
refreshQuantityOptions();
refreshDefaultUnitOptions();
this.updateSiDisplay();
}, 0);
}
parseDefinition() {
return splitValueAndUnit(this._el.si.value, 1);
}
getSiFactor(visited) {
visited = visited || new Set();
const { numVal, unitSuffix } = this.parseDefinition();
if (!unitSuffix || visited.has(this.id)) return numVal;
for (const u of $id("data-area").querySelectorAll("wc-unit")) {
if (u.querySelector(".unit-suffix").value === unitSuffix) {
visited.add(this.id);
return numVal * u.getSiFactor(visited);
}
}
return numVal;
}
saveData() {
return {
fullname: this._el.fullname.value,
type: this._el.type.value,
suffix: this._el.suffix.value,
si: this._el.si.value
};
}
restoreData(d) {
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() {
const { unitSuffix } = this.parseDefinition();
this._el.resolved.textContent = unitSuffix
? `= ${this.getSiFactor()} SI`
: "";
}
}
// ── WCBoolean — boolean variable ───────────────────────────────────────────
/** Boolean variable: name, true/false value, optional comment. Generates C `int`. */
class WCBoolean extends WCBlock {
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);
});
}
saveData() { return { name: this._el.name.value, value: this._el.value.value, comment: this._el.comment.value }; }
restoreData(d) {
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);
}
generateOwnCode() {
const name = toCName(this._el.name.value) || "flag";
const raw = this._el.value.value;
const numeric = raw === "true" ? 1 : 0;
const prefix = commentPrefix(this._el.name.value, this._el.comment.value);
return `${prefix}int ${name} = ${numeric};`;
}
}
// ── 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._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);
});
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();
});
}
parseValue() {
return splitValueAndUnit(this._el.value.value, 0);
}
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 { numVal } = this.parseValue();
const raw = (this._el.value.value || "0").trim();
if (unitSuffix && raw.split(/\s+/).length <= 1 && !isNaN(parseFloat(raw))) {
this._el.value.value = numVal + " " + 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,
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.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) || "x";
const typeName = this._el.type.value;
const si = this.siValue();
const prefix = commentPrefix(this._el.name.value, this._el.comment.value);
return typeName
? `${prefix}${typeName} ${name} = ${si};`
: `${prefix}double ${name} = ${si};`;
}
}
// ── 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._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);
});
}
saveData() { return { name: this._el.name.value, value: this._el.value.value, comment: this._el.comment.value }; }
restoreData(d) {
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);
}
generateOwnCode() {
const name = toCName(this._el.name.value) || "s";
const raw = this._el.value.value || "";
const escaped = escapeString(raw);
const len = raw.length;
const prefix = commentPrefix(this._el.name.value, this._el.comment.value);
return `${prefix}string ${name} = {0};\n${name}.length = ${len};\n${name}.data = malloc(${len} + 1);\nmemcpy(${name}.data, "${escaped}", ${len});\n${name}.data[${len}] = 0;\n`;
}
}
// ── 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"; }
connectedCallback() {
super.connectedCallback();
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);
}
_renderElements() {
const focused = this._el.elements.querySelector("input:focus");
let focusIdx = -1, focusOff = 0;
if (focused) {
const row = focused.closest(".array-element-row");
if (row && row.dataset.idx) {
focusIdx = parseInt(row.dataset.idx);
focusOff = focused.selectionStart;
}
}
this._el.elements.innerHTML = "";
for (let i = 0; i < this._values.length; i++) {
this._el.elements.appendChild(this._makeElementRow(i));
}
this._el.elements.appendChild(this._makeNewRow());
this._updateCount();
if (focusIdx >= 0 && focusIdx < this._values.length) {
const row = this._el.elements.querySelector(`[data-idx="${focusIdx}"]`);
if (row) {
const inp = row.querySelector(".array-element-value");
if (inp) { inp.focus(); inp.selectionStart = inp.selectionEnd = Math.min(focusOff, inp.value.length); }
}
}
}
_makeElementRow(i) {
const row = document.createElement("div");
row.className = "array-element-row";
row.dataset.idx = i;
const idxSpan = document.createElement("span");
idxSpan.className = "array-element-idx";
idxSpan.textContent = i;
row.appendChild(idxSpan);
const input = document.createElement("input");
input.className = "array-element-value";
input.value = this._values[i];
input.addEventListener("change", () => {
const ix = parseInt(input.closest(".array-element-row").dataset.idx);
this._values[ix] = input.value;
});
row.appendChild(input);
const delBtn = document.createElement("button");
delBtn.className = "array-element-del";
delBtn.textContent = "\u00D7";
delBtn.title = "remove";
delBtn.addEventListener("click", () => {
const ix = parseInt(delBtn.closest(".array-element-row").dataset.idx);
this._removeElement(ix);
});
row.appendChild(delBtn);
return row;
}
_makeNewRow() {
const row = document.createElement("div");
row.className = "array-element-row";
const idxSpan = document.createElement("span");
idxSpan.className = "array-element-idx";
idxSpan.textContent = this._values.length;
row.appendChild(idxSpan);
const input = document.createElement("input");
input.className = "array-element-value";
input.placeholder = "new element";
input.addEventListener("keydown", e => {
if (e.key === "Enter" && input.value.trim()) {
const val = input.value.trim();
input.value = "";
this._addElement(val);
input.focus();
}
});
input.addEventListener("blur", () => {
if (input.value.trim()) {
const val = input.value.trim();
input.value = "";
this._addElement(val);
}
});
row.appendChild(input);
return row;
}
_addElement(value) {
this._values.push(value);
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);
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;
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() {
return {
name: this._el.name.value,
type: this._el.type.value,
values: [...this._values],
comment: this._el.comment.value,
vecDim: this.getVecDim()
};
}
restoreData(d) {
this._el.name.value = d.name || "arr";
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) {
this._values = d.values.split(",").map(s => s.trim()).filter(s => s);
} else {
this._values = [];
}
this._renderElements();
}
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 && dim <= 1) {
return `${prefix}${type} ${name}[${totalSize}] = {${elements}};\n`;
}
return `${prefix}${type} ${name}[${totalSize}];\n`;
}
}