From f5f8141a86ec8c910288ed3cf7cd2587e2b09a2f Mon Sep 17 00:00:00 2001 From: Tokessa Hamann Date: Thu, 23 Jul 2026 15:29:12 +0200 Subject: [PATCH 1/7] added custom inputs for Light/Pointstatements and ControlFlow/ContactWaitstaements --- customWords.txt | 1 + frontend/blocks.js | 116 +++++++++++++++++++++---- frontend/dynamic_blocks.js | 172 ++++++++++++++++++++----------------- frontend/fields.js | 2 + frontend/generator.js | 63 ++++++++++++-- frontend/main.js | 95 +++++++++++++++++++- frontend/zelos_renderer.js | 142 ++++++++++++++++++++++++++++++ locales/de/de_locale.js | 4 +- locales/de/de_tokens.js | 8 +- locales/en/en_locale.js | 4 +- locales/en/en_tokens.js | 10 ++- package-lock.json | 27 +++--- 12 files changed, 517 insertions(+), 127 deletions(-) create mode 100644 frontend/zelos_renderer.js diff --git a/customWords.txt b/customWords.txt index 8218b0c..bb53339 100644 --- a/customWords.txt +++ b/customWords.txt @@ -10,3 +10,4 @@ railsl ebnf editorsprache setzanweisungen +zelos \ No newline at end of file diff --git a/frontend/blocks.js b/frontend/blocks.js index 16217fe..383a043 100644 --- a/frontend/blocks.js +++ b/frontend/blocks.js @@ -66,7 +66,7 @@ const blockDefinitionsJson = [ } ], output: 'CSetVector', - colour: 25, + colour: 0, tooltip: '%{BKY_RAILBLOCKS_STOP_TOOLTIP}' }, // Track-Statement Vector Direction case. @@ -91,7 +91,7 @@ const blockDefinitionsJson = [ ], output: 'CSetVector', tooltip: '%{BKY_RAILBLOCKS_DIR_TOOLTIP}', - colour: 25 + colour: 0 }, // Contact-Wait-Statement // Wait until a segment switch is reached or passed. @@ -100,13 +100,10 @@ const blockDefinitionsJson = [ message0: '%{BKY_RAILBLOCKS_CONTACT_WAIT_TEXT}', args0: [ { - type: 'field_dropdown', - options: [ - ['%{BKY_RAILBLOCKS_CONTACT_WAIT_REACHED}', 'ITEM1'], - ['%{BKY_RAILBLOCKS_CONTACT_WAIT_PASSED}', 'ITEM2'] - ], - name: 'CONTACT' - }, + type: "input_value", + name: "track_sensor_value", + check: "Boolean" + }, { type: 'field_dropdown', options: [ @@ -121,7 +118,7 @@ const blockDefinitionsJson = [ name: 'SEGMENT' } ], - colour: 240, + colour: 210, tooltip: '%{BKY_RAILBLOCKS_CONTACT_WAIT_TOOLTIP}', previousStatement: 'CStatement', nextStatement: 'CStatement' @@ -138,7 +135,7 @@ const blockDefinitionsJson = [ name: 'DURATION' } ], - colour: 240, + colour: 210, tooltip: '%{BKY_RAILBLOCKS_TIME_WAIT_TOOLTIP}', previousStatement: 'CStatement', nextStatement: 'CStatement' @@ -158,7 +155,7 @@ const blockDefinitionsJson = [ name: 'CROSSING_STATUS' } ], - colour: 280, + colour: 60, tooltip: '%{BKY_RAILBLOCKS_CROSSING_TOOLTIP}', previousStatement: 'CStatement', nextStatement: 'CStatement' @@ -188,6 +185,11 @@ const blockDefinitionsJson = [ check: 'CStatement', name: 'COND_BLOCK0' }, + { + type: "input_value", + name: "track_sensor_value", + check: "Boolean" + }, { type: 'field_dropdown', options: [ @@ -205,7 +207,12 @@ const blockDefinitionsJson = [ type: 'input_statement', check: 'CStatement', name: 'COND_BLOCK1' - } + }, + { + type: "input_value", + name: "track_sensor_value", + check: "Boolean" + } ], colour: 300, tooltip: '%{BKY_RAILBLOCKS_CONDITIONAL_TOOLTIP}', @@ -233,6 +240,59 @@ const blockDefinitionsJson = [ tooltip: '%{BKY_RAILBLOCKS_PARALLEL_TOOLTIP}', previousStatement: 'CStatement', nextStatement: 'CStatement' + }, + // Reached track sensor value + // Shows that we reached the track sensor. + { + type: "reached", + message0: "%{BKY_RAILBLOCKS_CONTACT_WAIT_REACHED}", + output: "Boolean", + colour: 155, + tooltip: "Returns true if reached" + }, + // Passed track sensor value + // Shows that we passed the track sensor. + { + type: "passed", + message0: "%{BKY_RAILBLOCKS_CONTACT_WAIT_PASSED}", + output: "Boolean", + colour: 155, + tooltip: "Returns true if passed" + }, + // Number range + // Is a range of numbers from start to end inclusive + { + type: "number_range", + message0: "%{BKY_RAILBLOCKS_NUMBER_RANGE_TEXT}", + args0: [ + { + type: "field_number", + name: "START", + value: 0 + }, + { + type: "field_number", + name: "END", + value: 2 + } + ], + output: "number_range", + colour: 80, + tooltip: "%{BKY_RAILBLOCKS_NUMBER_RANGE_TOOLTIP}" + }, + // Number + // Is just a simple number for shadow fields + { + type: "math_number", + message0: "%1", + args0: [ + { + type: "field_number", + name: "NUM", + value: 1 + } + ], + output: "Number" } ] @@ -264,11 +324,11 @@ function createToolbox (labels) { }, { kind: 'block', - type: 'CrossingStatement' + type: 'LightStatement', }, { kind: 'block', - type: 'LightStatement' + type: 'CrossingStatement' }, { kind: 'sep', @@ -305,7 +365,31 @@ function createToolbox (labels) { { kind: 'block', type: 'LoopStatement' - } + }, + { + kind: 'sep', + gap: 60 + }, + { + kind: 'label', + text: labels.sensorValues + }, + { + kind: 'block', + type: 'reached' + }, + { + kind: 'block', + type: 'passed' + }, + { + kind: 'label', + text: labels.numbers + }, + { + kind: 'block', + type: 'number_range' + }, ] } } diff --git a/frontend/dynamic_blocks.js b/frontend/dynamic_blocks.js index 6ab7e23..5722c5b 100644 --- a/frontend/dynamic_blocks.js +++ b/frontend/dynamic_blocks.js @@ -36,7 +36,7 @@ Blockly.Blocks.ParallelStatementD = { this.setPreviousStatement('CStatement') this.setNextStatement('CStatement') - this.setColour(190) + this.setColour(280) this.setTooltip(Blockly.Msg.RAILBLOCKS_PARALLEL_TOOLTIP) // We need at least two inputs, so we cheat in one that will always be there. @@ -87,13 +87,30 @@ Blockly.Blocks.ParallelStatementD = { // Small helper function that adds another block in a dynamic branch statement. // Remember to clean up inputs, when deleting. function buildAnotherCondRow(index, block){ - block.appendDummyInput(`COND_BLOCK${index}_TEXT`) - .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TEXT_START) - .appendField(new Blockly.FieldDropdown([[Blockly.Msg.RAILBLOCKS_CONDITIONAL_FIRST, 'ITEM1'], [Blockly.Msg.RAILBLOCKS_CONDITIONAL_SECOND, 'ITEM2']]), `CONTACT${index}`) - .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TEXT_MIDDLE) - .appendField(new Blockly.FieldDropdown(segName), `SEGMENT${index}`) - .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TEXT_END) - block.appendStatementInput(`COND_BLOCK${index}`) + const input = block.appendValueInput(`COND_BOOL${index}`) + .setCheck('Boolean') + + if (index !== 0) { + input.appendField(new Blockly.FieldLabel(' ')) // empty placeholder + } + + input + .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TEXT_START) + .appendField( + new Blockly.FieldDropdown([ + [Blockly.Msg.RAILBLOCKS_CONDITIONAL_FIRST, 'ITEM1'], + [Blockly.Msg.RAILBLOCKS_CONDITIONAL_SECOND, 'ITEM2'] + ]), + `CONTACT${index}` + ) + .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TEXT_MIDDLE) + .appendField(new Blockly.FieldDropdown(segName), `SEGMENT${index}`) + .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TEXT_END_1) + +block.appendDummyInput(`COND_DUMMY${index}`) + .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TEXT_END_2) + +block.appendStatementInput(`COND_BLOCK${index}`) } // Dynamic branch statement @@ -102,7 +119,6 @@ Blockly.Blocks.ConditionalStatementD = { // Append a named input which contains the +/- buttons and the text. this.appendDummyInput('BRANCH_ROOT') .appendField(new FieldPlusMinus(), 'PM_FIELD') - .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TITLE_TEXT) // Only allow this block to follow statements and only allow statements to be appended to this. this.setPreviousStatement('CStatement') @@ -140,8 +156,9 @@ Blockly.Blocks.ConditionalStatementD = { while (diff !== 0) { // Case when inputs have to be removed. if (diff > 0) { + this.removeInput(`COND_BOOL${this.currInputs}`) + this.removeInput(`COND_DUMMY${this.currInputs}`) this.removeInput(`COND_BLOCK${this.currInputs}`) - this.removeInput(`COND_BLOCK${this.currInputs}_TEXT`) this.currInputs-- // Case when inputs have to be appended. } else if (diff < 0) { @@ -169,7 +186,7 @@ Blockly.Blocks.TrackStatement = { this.setPreviousStatement('CStatement') this.setNextStatement('CStatement') - this.setColour(25) + this.setColour(0) this.setTooltip(Blockly.Msg.RAILBLOCKS_TRACK_TOOLTIP) this.inputCount = 1 @@ -242,7 +259,7 @@ Blockly.Blocks.TrackStatementALT = { this.setPreviousStatement('CStatement') this.setNextStatement('CStatement') this.setInputsInline(false) - this.setColour(25) + this.setColour(0) this.setTooltip(Blockly.Msg.RAILBLOCKS_TRACK_TOOLTIP) this.inputCount = 1 @@ -299,7 +316,7 @@ Blockly.Blocks.PointStatement = { .appendField(Blockly.Msg.RAILBLOCKS_POINT_TEXT_START, 'SET_POINT_FIELD') this.setPreviousStatement('CStatement') this.setNextStatement('CStatement') - this.setColour(0) + this.setColour(20) this.setTooltip(Blockly.Msg.RAILBLOCKS_POINT_TOOLTIP) this.inputCount = 1 @@ -311,42 +328,32 @@ Blockly.Blocks.PointStatement = { domToMutation, updateShape: function () { - const values = [] - for (let i = 0; i < this.inputCount; i++) { - values.push( - this.getField('NUMBER' + i) - ? this.getField('NUMBER' + i).getValue() - : '0' - ) - } + let i = 0 + while (this.getInput('NUMBER_INPUT_' + i)) { + this.removeInput('NUMBER_INPUT_' + i) + i++ + } - const dummyInput = this.getInput('SET_POINT') - const toRemove = [] + // Remove the old trailing dummy + if (this.getInput('POINT_END')) { + this.removeInput('POINT_END') + } - for (let i = 0; i < dummyInput.fieldRow.length; i++) { - const name = dummyInput.fieldRow[i].name - if (!name || name[0] === 'N' || name[0] === 'B') { toRemove.push(name) } - } - for (let i = 0; i < toRemove.length; i++) { - dummyInput.removeField(toRemove[i]) - } + for (let i = 0; i < this.inputCount; i++) { + const input = this.appendValueInput('NUMBER_INPUT_' + i) + .setCheck(['Number', 'number_range']) - for (let i = 0; i < this.inputCount; i++) { - if (i === 0) { - dummyInput.appendField(new Blockly.FieldNumber( - 0, 0, 29 - ), 'NUMBER' + i) - } else { - dummyInput.appendField(',') - .appendField(new Blockly.FieldNumber( - 0, 0, 29 - ), 'NUMBER' + i) + if (i !== 0) { + input.appendField(',') + } + + // Attach a shadow math_number block via XML injection + ensureShadow(this, 'NUMBER_INPUT_' + i) } - this.setFieldValue(values[i], 'NUMBER' + i) - } // Append the last portion of this block. - dummyInput.appendField(Blockly.Msg.RAILBLOCKS_POINT_TEXT_END) + this.appendDummyInput('POINT_END') + .appendField(Blockly.Msg.RAILBLOCKS_POINT_TEXT_END) .appendField(new Blockly.FieldDropdown( [ [Blockly.Msg.RAILBLOCKS_POINT_STRAIGHT, 'ITEM1'], @@ -363,7 +370,7 @@ Blockly.Blocks.LightStatement = { .appendField(Blockly.Msg.RAILBLOCKS_LIGHTS_TEXT_START, 'SET_LIGHT_FIELD') this.setPreviousStatement('CStatement') this.setNextStatement('CStatement') - this.setColour(60) + this.setColour(40) this.setTooltip(Blockly.Msg.RAILBLOCKS_LIGHTS_TOOLTIP) this.inputCount = 1 @@ -374,47 +381,50 @@ Blockly.Blocks.LightStatement = { domToMutation, - updateShape: function () { - const values = [] - for (let i = 0; i < this.inputCount; i++) { - values.push( - this.getField('NUMBER' + i) - ? this.getField('NUMBER' + i).getValue() - : '0' - ) - } + updateShape: function () { + let i = 0 + while (this.getInput('NUMBER_INPUT_' + i)) { + this.removeInput('NUMBER_INPUT_' + i) + i++ + } - const valueInput = this.getInput('SET_LIGHT') - const toRemove = [] + // Remove the old trailing dummy + if (this.getInput('LIGHT_END')) { + this.removeInput('LIGHT_END') + } - for (let i = 0; i < valueInput.fieldRow.length; i++) { - const name = valueInput.fieldRow[i].name - if (!name || name[0] === 'N' || name[0] === 'L') { toRemove.push(name) } - } - for (let i = 0; i < toRemove.length; i++) { - valueInput.removeField(toRemove[i]) - } + for (let i = 0; i < this.inputCount; i++) { + const input = this.appendValueInput('NUMBER_INPUT_' + i) + .setCheck(['Number', 'number_range']) - for (let i = 0; i < this.inputCount; i++) { - if (i === 0) { - valueInput.appendField(new Blockly.FieldNumber( - 0, 0, 23 - ), 'NUMBER' + i) - } else { - valueInput.appendField(',') - .appendField(new Blockly.FieldNumber( - 0, 0, 23 - ), 'NUMBER' + i) + if (i !== 0) { + input.appendField(',') + } + + // Only inject shadows when not loading from XML + ensureShadow(this, 'NUMBER_INPUT_' + i) } - this.setFieldValue(values[i], 'NUMBER' + i) + + this.appendDummyInput('LIGHT_END') + .appendField(Blockly.Msg.RAILBLOCKS_LIGHTS_TEXT_END) + .appendField(new Blockly.FieldDropdown( + [ + [Blockly.Msg.RAILBLOCKS_LIGHTS_ON, 'ITEM1'], + [Blockly.Msg.RAILBLOCKS_LIGHTS_OFF, 'ITEM2'] + ] + ), 'LIGHT_STATUS') } +} - valueInput.appendField(Blockly.Msg.RAILBLOCKS_LIGHTS_TEXT_END) - .appendField(new Blockly.FieldDropdown( - [ - [Blockly.Msg.RAILBLOCKS_LIGHTS_ON, 'ITEM1'], - [Blockly.Msg.RAILBLOCKS_LIGHTS_OFF, 'ITEM2'] - ] - ), 'LIGHT_STATUS') - } +// Injects a math_number shadow into an input if nothing is connected yet +export function ensureShadow(block, inputName) { + const input = block.getInput(inputName) + if (!input || input.connection.targetBlock()) return + + const shadowBlock = block.workspace.newBlock('math_number') + shadowBlock.initSvg?.() + shadowBlock.setShadow(true) + shadowBlock.getField('NUM').setValue(0) + shadowBlock.render?.() + input.connection.connect(shadowBlock.outputConnection) } diff --git a/frontend/fields.js b/frontend/fields.js index ebd2bfd..ad50b5e 100644 --- a/frontend/fields.js +++ b/frontend/fields.js @@ -20,6 +20,7 @@ import * as Blockly from 'blockly/core' import { BlockSvg } from 'blockly/core' +import { ensureShadow } from './dynamic_blocks' /** * Class for field that adds a Plus and Minus button that increment/decrement @@ -184,6 +185,7 @@ export class FieldPlusMinus extends Blockly.Field { increase_count () { this.sourceBlock_.inputCount++ this.sourceBlock_.updateShape() + ensureShadow(this.sourceBlock_, 'NUMBER_INPUT_' + (this.sourceBlock_.inputCount - 1)) } /** diff --git a/frontend/generator.js b/frontend/generator.js index 9f0c2d5..7e7899d 100644 --- a/frontend/generator.js +++ b/frontend/generator.js @@ -18,6 +18,7 @@ import * as Blockly from 'blockly/core' +import 'blockly/javascript' import { segNameMap } from './consts' // Enum internally used by Blockly. @@ -132,18 +133,42 @@ generator.forBlock.TrackStatementALT = (block, gen) => { return NEXT_COMMAND ? COMMAND + '\n' + gen.blockToCode(NEXT_COMMAND) : COMMAND } +// return a list of numbers from start to end inclusive +generator.forBlock.number_range = (block) => { + const start = parseInt(block.getFieldValue('START')) || 0 + const end = parseInt(block.getFieldValue('END')) || 0 + + const numbers = [] + if (start > end) { + for (let i = start; i >= end; i--) { + numbers.push(i) + } + } else { + for (let i = start; i <= end; i++) { + numbers.push(i) + } + } + + return [numbers.join(', '), Order.ATOMIC] +} + +// return a number +generator.forBlock.math_number = (block) => { + return [block.getFieldValue('NUM') || '0', Order.ATOMIC] +} + generator.forBlock.PointStatement = (block, gen) => { let COMMAND = 'Set point ' const tracks = [] for (let i = 0; i < block.inputCount; i++) { - tracks.push(block.getFieldValue('NUMBER' + i)) + tracks.push(gen.valueToCode(block, 'NUMBER_INPUT_' + i, Order.ATOMIC)) } // Same thing as before. const BRANCH_OPTION = ['straight', 'branch'][block.getFieldValue('BRANCH_OPTION')[4] - 1] - COMMAND += tracks.toString() + ' to ' + BRANCH_OPTION + '.' + COMMAND += tracks.join(', ') + ' to ' + BRANCH_OPTION + '.' const NEXT_COMMAND = block.getNextBlock() return NEXT_COMMAND ? COMMAND + '\n' + gen.blockToCode(NEXT_COMMAND) : COMMAND @@ -154,23 +179,39 @@ generator.forBlock.LightStatement = (block, gen) => { const tracks = [] for (let i = 0; i < block.inputCount; i++) { - tracks.push(block.getFieldValue('NUMBER' + i)) + tracks.push(gen.valueToCode(block, 'NUMBER_INPUT_' + i, Order.ATOMIC)) } // Same thing as before. const LIGHT_STATUS = ['on', 'off'][block.getFieldValue('LIGHT_STATUS')[4] - 1] - COMMAND += tracks.toString() + ' ' + LIGHT_STATUS + '.' + COMMAND += tracks.join(', ') + ' ' + LIGHT_STATUS + '.' const NEXT_COMMAND = block.getNextBlock() return NEXT_COMMAND ? COMMAND + '\n' + gen.blockToCode(NEXT_COMMAND) : COMMAND } +// return either reached or Reach depending on the block because of railsl +generator.forBlock.reached = (block) => { + if (block.parentBlock_ && block.parentBlock_.type === "ConditionalStatementD") { + return ['reached', Order.ATOMIC] + } + return ['Reach', Order.ATOMIC] +} + +// return either passed or Pass depending on the block because of railsl +generator.forBlock.passed = (block) => { + if (block.parentBlock_ && block.parentBlock_.type === "ConditionalStatementD") { + return ['passed', Order.ATOMIC] + } + return ['Pass', Order.ATOMIC] +} + generator.forBlock.ContactWaitStatement = (block, gen) => { const NUMBER = ['first', 'second'][block.getFieldValue('NUMBER')[4] - 1] - const CONTACT = ['Reach', 'Pass'][block.getFieldValue('CONTACT')[4] - 1] + const TRACK_SENSOR_VALUE = gen.valueToCode(block, 'track_sensor_value', Order.ATOMIC) const SEGMENT = segNameMap.get(block.getFieldValue('SEGMENT')) - const COMMAND = CONTACT + ' ' + NUMBER + ' contact of ' + SEGMENT + '.' + const COMMAND = TRACK_SENSOR_VALUE + ' ' + NUMBER + ' contact of ' + SEGMENT + '.' const NEXT_COMMAND = block.getNextBlock() return NEXT_COMMAND ? COMMAND + '\n' + gen.blockToCode(NEXT_COMMAND) : COMMAND @@ -240,13 +281,16 @@ generator.forBlock.ConditionalStatementD = (block, gen) => { const COND_BLOCK = gen.statementToCode(block, 'COND_BLOCK' + i) const CONTACT = ['first', 'second'][block.getFieldValue('CONTACT' + i)[4] - 1] const SEGMENT = segNameMap.get(block.getFieldValue('SEGMENT' + i)) + const TRACK_SENSOR_VALUE = gen.valueToCode(block, 'COND_BOOL' + i, Order.ATOMIC) COMMAND += 'If ' + CONTACT + ' contact of ' + SEGMENT + - ' is reached first, do\n' + + ' is ' + + TRACK_SENSOR_VALUE + + ' first, do\n' + 'Start:\n' + COND_BLOCK + '\nEnd.\n' @@ -263,6 +307,7 @@ generator.forBlock.ConditionalStatement = (block, gen) => { const CONTACT1 = ['first', 'second'][block.getFieldValue('CONTACT1')[4] - 1] const SEGMENT0 = segNameMap.get(block.getFieldValue('SEGMENT0')) const SEGMENT1 = segNameMap.get(block.getFieldValue('SEGMENT1')) + const TRACK_SENSOR_VALUE = gen.valueToCode(block, 'track_sensor_value', Order.ATOMIC) const COND_BLOCK0 = gen.statementToCode(block, 'COND_BLOCK0') const COND_BLOCK1 = gen.statementToCode(block, 'COND_BLOCK1') @@ -274,7 +319,9 @@ generator.forBlock.ConditionalStatement = (block, gen) => { CONTACT0 + ' contact of ' + SEGMENT0 + - ' is reached first, do\n' + + ' is ' + + TRACK_SENSOR_VALUE + + ' first, do\n' + 'Start:\n' + COND_BLOCK0 + '\nEnd.\n' + diff --git a/frontend/main.js b/frontend/main.js index 08c90f2..9add896 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -19,7 +19,7 @@ import * as Blockly from 'blockly/core' -import './renderer.js' +import './zelos_renderer.js' import './dynamic_blocks.js' import { blockDefinitionsJson, createToolbox } from './blocks.js' import { compile } from './generator.js' @@ -89,7 +89,7 @@ const workspace = Blockly.inject('blocklyDiv', { theme, // Load the custom renderer defined in renderer.js // Should not make a big difference but is advised to be loaded for extensions. - renderer: 'thrasos_extended', + renderer: 'zelos_renderer', move: { scrollbars: { horizontal: true, @@ -115,6 +115,7 @@ function restoreWorkspaceState (workspace) { const state = JSON.parse(savedWorkspace) Blockly.serialization.workspaces.load(state, workspace) window.localStorage.removeItem(workspaceStorageKey) + workspace.render() return true } catch (error) { window.localStorage.removeItem(workspaceStorageKey) @@ -210,8 +211,15 @@ function removeBlockWarning (block, warningKey) { function markUnusedBlocks (workspace) { // Mark all unused blocks workspace.getAllBlocks().forEach(block => { + // skip for shadow blocks; only show for parents + if (block.shadow) return + if ((block.type === 'number_range' || block.type === 'reached' || block.type === 'passed') && block.parentBlock_ !== null) { + removeBlockWarning(block, 'unused') + block.unused = false + } + // getRootBlock() returns the topmost block in a stack. - if (!block.unused && block.getRootBlock().id !== 'ROOT') { + else if (!block.unused && block.getRootBlock().id !== 'ROOT') { addBlockWarning(block, 'unused', Blockly.Msg.RAILBLOCKS_WARNING_UNUSED) block.unused = true } else if (block.unused && block.getRootBlock().id === 'ROOT') { @@ -221,6 +229,72 @@ function markUnusedBlocks (workspace) { }) } +/** + * Warns the user by indicating all ConditionalStatementD blocks that have a passed inside them. + * @param {Blockly.WorkspaceSvg} workspace The workspace to scan. + */ +function markPassedConditionalStatement (workspace) { + workspace.getAllBlocks().forEach(block => { + // is "passed" block in ConditionalStatementD? + if (block.type === 'passed' && block.parentBlock_?.type === 'ConditionalStatementD') { + const parent = block.parentBlock_ + if (!parent.passed) { + parent.passed = true + addBlockWarning(parent, 'passed', Blockly.Msg.RAILBLOCKS_WARNING_PASSED) + } + } else if (block.passed) { + // remove passed when child block no longer exists + const hasPassed = block.getChildren().some(child => child.type === 'passed') + block.passed = hasPassed + if (!hasPassed) { + removeBlockWarning(block, 'passed') + } + } + }) +} + +/** + * Warns the user by indicating all blocks that are not inside the constrains. + * @param {Blockly.WorkspaceSvg} workspace The workspace to scan. + */ +function markBrokenConstrains (workspace) { + // Mark all blocks with broken constrains + const validators = { + // check number block + math_number: (block) => { + const value = Number(block.getFieldValue('NUM')) + return value < 0 || value > 23 + }, + // check both parts of range block + number_range: (block) => { + const start = Number(block.getFieldValue('START')) + const end = Number(block.getFieldValue('END')) + return start < 0 || start > 23 || end < 0 || end > 23 + } + } + + // reset all brokenConstraint flags + workspace.getAllBlocks().forEach(block => { + block.brokenConstraint = false + removeBlockWarning(block, 'broken_constraint') + }) + + // accumulate broken state onto target + workspace.getAllBlocks().forEach(block => { + const validate = validators[block.type] + if (!validate) return + + const isBroken = validate(block) + // set warning on parent block if it exists + const target = block.parentBlock_ ?? block + + if (isBroken) { + target.brokenConstraint = true + addBlockWarning(target, 'broken_constraint', Blockly.Msg.RAILBLOCKS_WARNING_BROKEN_CONSTRAIN) + } + }) +} + /** * Helper function that checks if a block with input statements contains or is a loop statement. * @param {Blockly.Block} block Block to be searched in. @@ -302,12 +376,24 @@ workspace.addChangeListener((event) => { // Send all code to the output div. compile(workspace) - if (event.type === Blockly.Events.BLOCK_MOVE) { + if (event.type === Blockly.Events.BLOCK_MOVE || event.type === Blockly.Events.FINISHED_LOADING) { // Mark blocks not inside the program block. markUnusedBlocks(workspace) // Mark Branch and Parallel blocks if they contain a loop. markWarnings(workspace) } + if (event.type === Blockly.Events.BLOCK_FIELD_INTERMEDIATE_CHANGE || + event.type === Blockly.Events.BLOCK_DRAG || + event.type === Blockly.Events.BLOCK_CHANGE || + event.type === Blockly.Events.FINISHED_LOADING) { + // Mark blocks that have numbers outside of the constrains + markBrokenConstrains(workspace) + } + if (event.type === Blockly.Events.BLOCK_DRAG) { + // Mark ConditionalStatementD blocks that have a "passed" block in themself + markPassedConditionalStatement(workspace) + } + // Mark blocks that have unconnected connections. markUnconnectedBlocks(workspace) @@ -459,6 +545,7 @@ document.getElementById('file_load').addEventListener('change', (event) => { // (toString because type safety) const state = JSON.parse(fr.result.toString()) Blockly.serialization.workspaces.load(state, workspace) + workspace.render() } // ... and load it. fr.readAsText(files[0]) diff --git a/frontend/zelos_renderer.js b/frontend/zelos_renderer.js new file mode 100644 index 0000000..aeeb303 --- /dev/null +++ b/frontend/zelos_renderer.js @@ -0,0 +1,142 @@ +/* + * RailBlocks - A Blockly RailSL Implementation + * + * https://github.com/kieler/RailBlocks + * + * Copyright 2026 by + * + Tokessa Hamann and + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made + * available under the terms of the MIT License which + * is available at https://opensource.org/license/MIT. + * + * SPDX-License-Identifier: MIT + */ + +import * as Blockly from 'blockly/core' + +// CUSTOM BUILD RENDERER TO IMPLEMENT EASY NEW CONNECTION SHAPES + +/** + * Class extending the Zelos renderer constants to define custom shapes in and add them to connection checks. + */ +class ZelosExtendedConstantProvider extends Blockly.zelos.Renderer { + constructor(name) { + super(name); + } + + makeConstants_() { + return new CustomConstants() + } +} + +class CustomConstants extends Blockly.zelos.ConstantProvider { + init() { + super.init() + + const maxWidth = this.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH; + + function lineTo(dx, dy) { + return ' l ' + dx + ',' + dy + ' '; + } + + + // creates Hexagon Shape (currently not in use) + // eslint-disable-next-line no-unused-vars + function makeMainPathHex(height, up, right) { + const halfHeight = height / 2; + const width = halfHeight > maxWidth ? maxWidth : halfHeight; + const forward = up ? -1 : 1; + const direction = right ? -1 : 1; + + const cornerSize = width / 2; // diagonal corner cut size + const dyCorner = (forward * height) / 4; + const dyFlat = (forward * height) / 2 - dyCorner; + + return ( + lineTo(-direction * cornerSize, dyCorner) + + lineTo(-direction * (width - cornerSize), dyFlat) + + lineTo(direction * (width - cornerSize), dyFlat) + + lineTo(direction * cornerSize, dyCorner) + ); + } + + // Creates Octagon shape + function makeMainPathOct(height, up, right) { + const halfHeight = height / 2; + const width = halfHeight > maxWidth ? maxWidth : halfHeight; + const forward = up ? -1 : 1; + const direction = right ? -1 : 1; + + // Split the half-height into 3 parts: corner / straight / corner + const cornerHeight = halfHeight / 2; + const straightHeight = halfHeight; + const cornerWidth = width / 2; + + const dyCorner = forward * cornerHeight; + const dyStraight = forward * straightHeight; + + return ( + // top/bottom flat horizontal edge + lineTo(-direction * (width - cornerWidth), 0) + + // diagonal corner + lineTo(-direction * cornerWidth, dyCorner) + + // vertical edge + lineTo(0, dyStraight) + + // diagonal corner (mirrors the first) + lineTo(direction * cornerWidth, dyCorner) + + // bottom/top flat horizontal edge + lineTo(direction * (width - cornerWidth), 0) + ); + } + + this.C_SET_VECTOR_SHAPE = { + type: this.SHAPES.HEXAGONAL, + isDynamic: true, + width(height) { + const halfHeight = height / 2; + return halfHeight > maxWidth ? maxWidth : halfHeight; + }, + height(height) { + return height; + }, + connectionOffsetY(connectionHeight) { + return connectionHeight / 2; + }, + connectionOffsetX(connectionWidth) { + return -connectionWidth; + }, + pathDown(height) { + return makeMainPathOct(height, false, false); + }, + pathUp(height) { + return makeMainPathOct(height, true, false); + }, + pathRightDown(height) { + return makeMainPathOct(height, false, true); + }, + pathRightUp(height) { + return makeMainPathOct(height, false, true); + }, + } + } + + shapeFor(connection) { + let check = connection.getCheck() + + if (!check && connection.targetConnection) { + check = connection.targetConnection.getCheck() + } + + if (check && check.includes('CSetVector')) return this.C_SET_VECTOR_SHAPE + + return super.shapeFor(connection) + + } +} + +Blockly.blockRendering.register('zelos_renderer', ZelosExtendedConstantProvider); + diff --git a/locales/de/de_locale.js b/locales/de/de_locale.js index 6927ae0..f1fa0ea 100644 --- a/locales/de/de_locale.js +++ b/locales/de/de_locale.js @@ -40,6 +40,8 @@ export const de_locale = { toolboxLabels: { setStatements: 'Setzanweisungen', waitStatements: 'Warteanweisungen', - controlFlow: 'Kontrollfluss' + controlFlow: 'Kontrollfluss', + sensorValues: 'Gleissensorwerte', + numbers: 'Nummern', } }; diff --git a/locales/de/de_tokens.js b/locales/de/de_tokens.js index 0f34655..8379f0b 100644 --- a/locales/de/de_tokens.js +++ b/locales/de/de_tokens.js @@ -39,6 +39,9 @@ export const de_tokens = { RAILBLOCKS_CONTACT_WAIT_FIRST: 'erster', RAILBLOCKS_CONTACT_WAIT_SECOND: 'zweiter', + RAILBLOCKS_NUMBER_RANGE_TEXT: '%1 bis %2', + RAILBLOCKS_NUMBER_RANGE_TOOLTIP: 'Erstellt eine Reihe von Nummern.', + RAILBLOCKS_TIME_WAIT_TEXT: 'Warte für %1 Sekunde(n)', RAILBLOCKS_TIME_WAIT_TOOLTIP: 'Tue nichts für eine bestimmte Zeit.', @@ -53,7 +56,8 @@ export const de_tokens = { RAILBLOCKS_CONDITIONAL_SECOND: 'zweiter', RAILBLOCKS_CONDITIONAL_TEXT_START: 'Wenn', RAILBLOCKS_CONDITIONAL_TEXT_MIDDLE: 'Kontakt von', - RAILBLOCKS_CONDITIONAL_TEXT_END: 'zuerst ausgelöst wurde', + RAILBLOCKS_CONDITIONAL_TEXT_END_1: 'zuerst', + RAILBLOCKS_CONDITIONAL_TEXT_END_2: 'wurde', RAILBLOCKS_CONDITIONAL_TITLE_TEXT: 'Entweder Oder', RAILBLOCKS_PARALLEL_TEXT: 'Parallel', @@ -79,4 +83,6 @@ export const de_tokens = { RAILBLOCKS_WARNING_UNREACHABLE_WEAK: 'Blöcke nach diesem können vielleicht nicht erreicht werden, da sich eine Wiederholung innerhalb dieses Blocks befindet.', RAILBLOCKS_WARNING_EMPTY_INPUT: 'Dieser Block hat leere Felder und wird einen Syntaxfehler verursachen!', RAILBLOCKS_WARNING_UNUSED: 'Unbenutzter Block', + RAILBLOCKS_WARNING_BROKEN_CONSTRAIN: 'Dieser Block enthält Zahlen, welche außerhalb der Einschränkungen liegen.', + RAILBLOCKS_WARNING_PASSED: 'Der "passiert" Block kann momentan nicht in diesem Block genutzt werden.', } diff --git a/locales/en/en_locale.js b/locales/en/en_locale.js index 4347367..e29104a 100644 --- a/locales/en/en_locale.js +++ b/locales/en/en_locale.js @@ -40,6 +40,8 @@ export const en_locale = { toolboxLabels: { setStatements: 'Set Statements', waitStatements: 'Wait Statements', - controlFlow: 'Control Flow' + controlFlow: 'Control Flow', + sensorValues: 'Track Sensor Values', + numbers: 'Numbers', } }; diff --git a/locales/en/en_tokens.js b/locales/en/en_tokens.js index eb49cdd..10a7cd0 100644 --- a/locales/en/en_tokens.js +++ b/locales/en/en_tokens.js @@ -39,6 +39,9 @@ export const en_tokens = { RAILBLOCKS_CONTACT_WAIT_FIRST: 'first', RAILBLOCKS_CONTACT_WAIT_SECOND: 'second', + RAILBLOCKS_NUMBER_RANGE_TEXT: '%1 to %2', + RAILBLOCKS_NUMBER_RANGE_TOOLTIP: 'Creates a range of numbers.', + RAILBLOCKS_TIME_WAIT_TEXT: 'Wait for %1 second(s)', RAILBLOCKS_TIME_WAIT_TOOLTIP: 'Do nothing for a certain amount of time.', @@ -47,13 +50,14 @@ export const en_tokens = { RAILBLOCKS_CROSSING_OPEN: 'Open', RAILBLOCKS_CROSSING_CLOSE: 'Close', - RAILBLOCKS_CONDITIONAL_TEXT: 'If %1 contact of %2 is reached first\n%3else if %4 contact of %5 is reached first', + RAILBLOCKS_CONDITIONAL_TEXT: 'If %1 contact of %2 is %3 first\n else if %4 contact of %5 is %6 first', RAILBLOCKS_CONDITIONAL_TOOLTIP: 'Execute different blocks depending on which track is reached first by a train.', RAILBLOCKS_CONDITIONAL_FIRST: 'first', RAILBLOCKS_CONDITIONAL_SECOND: 'second', RAILBLOCKS_CONDITIONAL_TEXT_START: 'If', RAILBLOCKS_CONDITIONAL_TEXT_MIDDLE: 'contact of', - RAILBLOCKS_CONDITIONAL_TEXT_END: 'is reached first', + RAILBLOCKS_CONDITIONAL_TEXT_END_1: 'is', + RAILBLOCKS_CONDITIONAL_TEXT_END_2: 'first', RAILBLOCKS_CONDITIONAL_TITLE_TEXT: 'Branch', RAILBLOCKS_PARALLEL_TEXT: 'Parallel', @@ -79,4 +83,6 @@ export const en_tokens = { RAILBLOCKS_WARNING_UNREACHABLE_WEAK: 'Blocks after this may not be reached because of a loop inside this.', RAILBLOCKS_WARNING_EMPTY_INPUT: 'This block has empty inputs and will cause a syntax error!', RAILBLOCKS_WARNING_UNUSED: 'Unused block', + RAILBLOCKS_WARNING_BROKEN_CONSTRAIN: 'This block has numbers that are outside of the constrains.', + RAILBLOCKS_WARNING_PASSED: 'The "passed" block can currently not be used in this block.', } diff --git a/package-lock.json b/package-lock.json index 779c0d2..21b8943 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2393,14 +2393,14 @@ } }, "node_modules/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.3.tgz", + "integrity": "sha512-ihjs0E2SxvDgq/MK418hX6YycQgKhsqxpbZuZbHo0yKfqDWdymWMjWYIpCIzqDDLLKClHlXev8whW/8WXmJ0BA==", "license": "MIT", "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", - "shell-quote": "1.8.3", + "shell-quote": "1.8.4", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" @@ -4320,9 +4320,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4746,9 +4746,9 @@ } }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -4824,6 +4824,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", @@ -5056,9 +5057,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" From 6f2ccd5c9f3911a43ffae966f38a9bd5c27ca4f6 Mon Sep 17 00:00:00 2001 From: Tokessa Hamann Date: Mon, 27 Jul 2026 10:03:08 +0200 Subject: [PATCH 2/7] better plusMinus update --- frontend/dynamic_blocks.js | 73 ++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/frontend/dynamic_blocks.js b/frontend/dynamic_blocks.js index 5722c5b..fab7718 100644 --- a/frontend/dynamic_blocks.js +++ b/frontend/dynamic_blocks.js @@ -328,29 +328,34 @@ Blockly.Blocks.PointStatement = { domToMutation, updateShape: function () { - let i = 0 - while (this.getInput('NUMBER_INPUT_' + i)) { - this.removeInput('NUMBER_INPUT_' + i) - i++ + let currentCount = 0 + while (this.getInput('NUMBER_INPUT_' + currentCount)) { + currentCount++ } - // Remove the old trailing dummy + // Remove the trailing dummy so new number inputs can stay before it. if (this.getInput('POINT_END')) { this.removeInput('POINT_END') } - for (let i = 0; i < this.inputCount; i++) { - const input = this.appendValueInput('NUMBER_INPUT_' + i) + while (currentCount > this.inputCount) { + this.removeInput('NUMBER_INPUT_' + (currentCount - 1)) + currentCount-- + } + + while (currentCount < this.inputCount) { + const input = this.appendValueInput('NUMBER_INPUT_' + currentCount) .setCheck(['Number', 'number_range']) - if (i !== 0) { + if (currentCount !== 0) { input.appendField(',') } - // Attach a shadow math_number block via XML injection - ensureShadow(this, 'NUMBER_INPUT_' + i) - } - + // Only inject shadows when the input is newly created. + ensureShadow(this, 'NUMBER_INPUT_' + currentCount) + currentCount++ + } + // Append the last portion of this block. this.appendDummyInput('POINT_END') .appendField(Blockly.Msg.RAILBLOCKS_POINT_TEXT_END) @@ -382,37 +387,43 @@ Blockly.Blocks.LightStatement = { domToMutation, updateShape: function () { - let i = 0 - while (this.getInput('NUMBER_INPUT_' + i)) { - this.removeInput('NUMBER_INPUT_' + i) - i++ + let currentCount = 0 + while (this.getInput('NUMBER_INPUT_' + currentCount)) { + currentCount++ } - // Remove the old trailing dummy + // Remove the trailing dummy so new number inputs can stay before it. if (this.getInput('LIGHT_END')) { this.removeInput('LIGHT_END') } - for (let i = 0; i < this.inputCount; i++) { - const input = this.appendValueInput('NUMBER_INPUT_' + i) + while (currentCount > this.inputCount) { + this.removeInput('NUMBER_INPUT_' + (currentCount - 1)) + currentCount-- + } + + while (currentCount < this.inputCount) { + const input = this.appendValueInput('NUMBER_INPUT_' + currentCount) .setCheck(['Number', 'number_range']) - if (i !== 0) { + if (currentCount !== 0) { input.appendField(',') } - // Only inject shadows when not loading from XML - ensureShadow(this, 'NUMBER_INPUT_' + i) - } + // Only inject shadows when the input is newly created. + ensureShadow(this, 'NUMBER_INPUT_' + currentCount) + currentCount++ + } + + this.appendDummyInput('LIGHT_END') + .appendField(Blockly.Msg.RAILBLOCKS_LIGHTS_TEXT_END) + .appendField(new Blockly.FieldDropdown( + [ + [Blockly.Msg.RAILBLOCKS_LIGHTS_ON, 'ITEM1'], + [Blockly.Msg.RAILBLOCKS_LIGHTS_OFF, 'ITEM2'] + ] + ), 'LIGHT_STATUS') - this.appendDummyInput('LIGHT_END') - .appendField(Blockly.Msg.RAILBLOCKS_LIGHTS_TEXT_END) - .appendField(new Blockly.FieldDropdown( - [ - [Blockly.Msg.RAILBLOCKS_LIGHTS_ON, 'ITEM1'], - [Blockly.Msg.RAILBLOCKS_LIGHTS_OFF, 'ITEM2'] - ] - ), 'LIGHT_STATUS') } } From 680eeb3d3ded3eca2c2783678a4a75d5ee64781e Mon Sep 17 00:00:00 2001 From: Tokessa Hamann Date: Tue, 28 Jul 2026 15:56:29 +0200 Subject: [PATCH 3/7] small updates due to pull request feedback --- frontend/dynamic_blocks.js | 6 +++--- frontend/main.js | 10 ++++++---- locales/de/de_tokens.js | 2 +- locales/en/en_tokens.js | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/frontend/dynamic_blocks.js b/frontend/dynamic_blocks.js index fab7718..842b32a 100644 --- a/frontend/dynamic_blocks.js +++ b/frontend/dynamic_blocks.js @@ -90,9 +90,9 @@ function buildAnotherCondRow(index, block){ const input = block.appendValueInput(`COND_BOOL${index}`) .setCheck('Boolean') - if (index !== 0) { - input.appendField(new Blockly.FieldLabel(' ')) // empty placeholder - } + // if (index !== 0) { + // input.appendField(new Blockly.FieldLabel(' ')) // empty placeholder + // } input .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TEXT_START) diff --git a/frontend/main.js b/frontend/main.js index 9add896..9aab7f7 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -253,23 +253,25 @@ function markPassedConditionalStatement (workspace) { }) } +const light_constrains = [0, 23] + /** - * Warns the user by indicating all blocks that are not inside the constrains. + * Warns the user by indicating all blocks that are not inside the constraints. * @param {Blockly.WorkspaceSvg} workspace The workspace to scan. */ function markBrokenConstrains (workspace) { - // Mark all blocks with broken constrains + // Mark all blocks with broken constraints const validators = { // check number block math_number: (block) => { const value = Number(block.getFieldValue('NUM')) - return value < 0 || value > 23 + return value < light_constrains[0] || value > light_constrains[1] }, // check both parts of range block number_range: (block) => { const start = Number(block.getFieldValue('START')) const end = Number(block.getFieldValue('END')) - return start < 0 || start > 23 || end < 0 || end > 23 + return start < light_constrains[0] || start > light_constrains[1] || end < light_constrains[0] || end > light_constrains[1] } } diff --git a/locales/de/de_tokens.js b/locales/de/de_tokens.js index 8379f0b..5db76f4 100644 --- a/locales/de/de_tokens.js +++ b/locales/de/de_tokens.js @@ -83,6 +83,6 @@ export const de_tokens = { RAILBLOCKS_WARNING_UNREACHABLE_WEAK: 'Blöcke nach diesem können vielleicht nicht erreicht werden, da sich eine Wiederholung innerhalb dieses Blocks befindet.', RAILBLOCKS_WARNING_EMPTY_INPUT: 'Dieser Block hat leere Felder und wird einen Syntaxfehler verursachen!', RAILBLOCKS_WARNING_UNUSED: 'Unbenutzter Block', - RAILBLOCKS_WARNING_BROKEN_CONSTRAIN: 'Dieser Block enthält Zahlen, welche außerhalb der Einschränkungen liegen.', + RAILBLOCKS_WARNING_BROKEN_CONSTRAIN: 'Dieser Block enthält Zahlen, welche außerhalb des gültigen Bereichs liegen.', RAILBLOCKS_WARNING_PASSED: 'Der "passiert" Block kann momentan nicht in diesem Block genutzt werden.', } diff --git a/locales/en/en_tokens.js b/locales/en/en_tokens.js index 10a7cd0..4c0696a 100644 --- a/locales/en/en_tokens.js +++ b/locales/en/en_tokens.js @@ -83,6 +83,6 @@ export const en_tokens = { RAILBLOCKS_WARNING_UNREACHABLE_WEAK: 'Blocks after this may not be reached because of a loop inside this.', RAILBLOCKS_WARNING_EMPTY_INPUT: 'This block has empty inputs and will cause a syntax error!', RAILBLOCKS_WARNING_UNUSED: 'Unused block', - RAILBLOCKS_WARNING_BROKEN_CONSTRAIN: 'This block has numbers that are outside of the constrains.', + RAILBLOCKS_WARNING_BROKEN_CONSTRAIN: 'This block has numbers that are outside the valid range.', RAILBLOCKS_WARNING_PASSED: 'The "passed" block can currently not be used in this block.', } From 06f52d821784b5d9c2591f4e3f10d18f7c9e8bff Mon Sep 17 00:00:00 2001 From: Tokessa Hamann Date: Wed, 29 Jul 2026 09:16:49 +0200 Subject: [PATCH 4/7] added switch constrains --- frontend/main.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/frontend/main.js b/frontend/main.js index 9aab7f7..316f1c7 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -253,7 +253,20 @@ function markPassedConditionalStatement (workspace) { }) } +// constrains for number of lights and number of switch points const light_constrains = [0, 23] +const switch_constrains = [0, 29] + +// Map of parent block type to constraint range +const parentConstraints = { + PointStatement: switch_constrains +} +const defaultConstraints = light_constrains + +function getConstraintsFor (block) { + const parentType = block.parentBlock_?.type + return parentConstraints[parentType] || defaultConstraints +} /** * Warns the user by indicating all blocks that are not inside the constraints. @@ -264,14 +277,16 @@ function markBrokenConstrains (workspace) { const validators = { // check number block math_number: (block) => { + const [minimum, maximum] = getConstraintsFor(block) const value = Number(block.getFieldValue('NUM')) - return value < light_constrains[0] || value > light_constrains[1] + return value < minimum || value > maximum }, // check both parts of range block number_range: (block) => { + const [minimum, maximum] = getConstraintsFor(block) const start = Number(block.getFieldValue('START')) const end = Number(block.getFieldValue('END')) - return start < light_constrains[0] || start > light_constrains[1] || end < light_constrains[0] || end > light_constrains[1] + return start < minimum || start > maximum || end < minimum || end > maximum } } From 781f0a9042dd399abed7045f29dee452b216562a Mon Sep 17 00:00:00 2001 From: Tokessa Hamann Date: Sun, 2 Aug 2026 14:22:14 +0200 Subject: [PATCH 5/7] changed broken constraints warning to validator + added correct switch point constraints --- frontend/blocks.js | 30 ++++++++------ frontend/dynamic_blocks.js | 8 ++-- frontend/generator.js | 6 +-- frontend/main.js | 69 +------------------------------ frontend/validators.js | 85 ++++++++++++++++++++++++++++++++++++++ locales/de/de_tokens.js | 5 +-- locales/en/en_tokens.js | 5 +-- 7 files changed, 116 insertions(+), 92 deletions(-) create mode 100644 frontend/validators.js diff --git a/frontend/blocks.js b/frontend/blocks.js index 383a043..d52d118 100644 --- a/frontend/blocks.js +++ b/frontend/blocks.js @@ -259,16 +259,18 @@ const blockDefinitionsJson = [ colour: 155, tooltip: "Returns true if passed" }, - // Number range - // Is a range of numbers from start to end inclusive + // Integer range + // Is a range of integer numbers from start to end inclusive { - type: "number_range", - message0: "%{BKY_RAILBLOCKS_NUMBER_RANGE_TEXT}", + type: "int_range", + message0: "%{BKY_RAILBLOCKS_INT_RANGE_TEXT}", args0: [ { type: "field_number", name: "START", - value: 0 + value: 0, + min: 0, + precision: 1 }, { type: "field_number", @@ -276,23 +278,27 @@ const blockDefinitionsJson = [ value: 2 } ], - output: "number_range", + output: "int_range", + extensions: ["dynamic_int_range_validator"], colour: 80, - tooltip: "%{BKY_RAILBLOCKS_NUMBER_RANGE_TOOLTIP}" + tooltip: "%{BKY_RAILBLOCKS_INT_RANGE_TOOLTIP}" }, - // Number + // Integer // Is just a simple number for shadow fields { - type: "math_number", + type: "int_number", message0: "%1", args0: [ { type: "field_number", name: "NUM", - value: 1 + value: 1, + min: 0, + precision: 1 } ], - output: "Number" + output: "Number", + extensions: ["dynamic_int_number_validator"] } ] @@ -388,7 +394,7 @@ function createToolbox (labels) { }, { kind: 'block', - type: 'number_range' + type: 'int_range' }, ] } diff --git a/frontend/dynamic_blocks.js b/frontend/dynamic_blocks.js index 842b32a..b2343f3 100644 --- a/frontend/dynamic_blocks.js +++ b/frontend/dynamic_blocks.js @@ -345,7 +345,7 @@ Blockly.Blocks.PointStatement = { while (currentCount < this.inputCount) { const input = this.appendValueInput('NUMBER_INPUT_' + currentCount) - .setCheck(['Number', 'number_range']) + .setCheck(['Number', 'int_range']) if (currentCount !== 0) { input.appendField(',') @@ -404,7 +404,7 @@ Blockly.Blocks.LightStatement = { while (currentCount < this.inputCount) { const input = this.appendValueInput('NUMBER_INPUT_' + currentCount) - .setCheck(['Number', 'number_range']) + .setCheck(['Number', 'int_range']) if (currentCount !== 0) { input.appendField(',') @@ -427,12 +427,12 @@ Blockly.Blocks.LightStatement = { } } -// Injects a math_number shadow into an input if nothing is connected yet +// Injects a int_number shadow into an input if nothing is connected yet export function ensureShadow(block, inputName) { const input = block.getInput(inputName) if (!input || input.connection.targetBlock()) return - const shadowBlock = block.workspace.newBlock('math_number') + const shadowBlock = block.workspace.newBlock('int_number') shadowBlock.initSvg?.() shadowBlock.setShadow(true) shadowBlock.getField('NUM').setValue(0) diff --git a/frontend/generator.js b/frontend/generator.js index 7e7899d..77e808a 100644 --- a/frontend/generator.js +++ b/frontend/generator.js @@ -134,7 +134,7 @@ generator.forBlock.TrackStatementALT = (block, gen) => { } // return a list of numbers from start to end inclusive -generator.forBlock.number_range = (block) => { +generator.forBlock.int_range = (block) => { const start = parseInt(block.getFieldValue('START')) || 0 const end = parseInt(block.getFieldValue('END')) || 0 @@ -152,8 +152,8 @@ generator.forBlock.number_range = (block) => { return [numbers.join(', '), Order.ATOMIC] } -// return a number -generator.forBlock.math_number = (block) => { +// return an integer number +generator.forBlock.int_number = (block) => { return [block.getFieldValue('NUM') || '0', Order.ATOMIC] } diff --git a/frontend/main.js b/frontend/main.js index 316f1c7..5d0ef2e 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -21,6 +21,7 @@ import * as Blockly from 'blockly/core' import './zelos_renderer.js' import './dynamic_blocks.js' +import './validators.js' import { blockDefinitionsJson, createToolbox } from './blocks.js' import { compile } from './generator.js' import { getLabel, getToolBoxLabels, getHtmlLabels, getStoredLanguage, applyLanguage, LANGUAGE_STORAGE_KEY } from './localization.js' @@ -213,7 +214,7 @@ function markUnusedBlocks (workspace) { workspace.getAllBlocks().forEach(block => { // skip for shadow blocks; only show for parents if (block.shadow) return - if ((block.type === 'number_range' || block.type === 'reached' || block.type === 'passed') && block.parentBlock_ !== null) { + if ((block.type === 'int_range' || block.type === 'reached' || block.type === 'passed') && block.parentBlock_ !== null) { removeBlockWarning(block, 'unused') block.unused = false } @@ -253,65 +254,6 @@ function markPassedConditionalStatement (workspace) { }) } -// constrains for number of lights and number of switch points -const light_constrains = [0, 23] -const switch_constrains = [0, 29] - -// Map of parent block type to constraint range -const parentConstraints = { - PointStatement: switch_constrains -} -const defaultConstraints = light_constrains - -function getConstraintsFor (block) { - const parentType = block.parentBlock_?.type - return parentConstraints[parentType] || defaultConstraints -} - -/** - * Warns the user by indicating all blocks that are not inside the constraints. - * @param {Blockly.WorkspaceSvg} workspace The workspace to scan. - */ -function markBrokenConstrains (workspace) { - // Mark all blocks with broken constraints - const validators = { - // check number block - math_number: (block) => { - const [minimum, maximum] = getConstraintsFor(block) - const value = Number(block.getFieldValue('NUM')) - return value < minimum || value > maximum - }, - // check both parts of range block - number_range: (block) => { - const [minimum, maximum] = getConstraintsFor(block) - const start = Number(block.getFieldValue('START')) - const end = Number(block.getFieldValue('END')) - return start < minimum || start > maximum || end < minimum || end > maximum - } - } - - // reset all brokenConstraint flags - workspace.getAllBlocks().forEach(block => { - block.brokenConstraint = false - removeBlockWarning(block, 'broken_constraint') - }) - - // accumulate broken state onto target - workspace.getAllBlocks().forEach(block => { - const validate = validators[block.type] - if (!validate) return - - const isBroken = validate(block) - // set warning on parent block if it exists - const target = block.parentBlock_ ?? block - - if (isBroken) { - target.brokenConstraint = true - addBlockWarning(target, 'broken_constraint', Blockly.Msg.RAILBLOCKS_WARNING_BROKEN_CONSTRAIN) - } - }) -} - /** * Helper function that checks if a block with input statements contains or is a loop statement. * @param {Blockly.Block} block Block to be searched in. @@ -399,13 +341,6 @@ workspace.addChangeListener((event) => { // Mark Branch and Parallel blocks if they contain a loop. markWarnings(workspace) } - if (event.type === Blockly.Events.BLOCK_FIELD_INTERMEDIATE_CHANGE || - event.type === Blockly.Events.BLOCK_DRAG || - event.type === Blockly.Events.BLOCK_CHANGE || - event.type === Blockly.Events.FINISHED_LOADING) { - // Mark blocks that have numbers outside of the constrains - markBrokenConstrains(workspace) - } if (event.type === Blockly.Events.BLOCK_DRAG) { // Mark ConditionalStatementD blocks that have a "passed" block in themself markPassedConditionalStatement(workspace) diff --git a/frontend/validators.js b/frontend/validators.js new file mode 100644 index 0000000..41d1975 --- /dev/null +++ b/frontend/validators.js @@ -0,0 +1,85 @@ +/* + * RailBlocks - A Blockly RailSL Implementation + * + * https://github.com/kieler/RailBlocks + * + * Copyright 2025-2026 by + * + Tokessa Hamann and + * + Henri Heyden and + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made + * available under the terms of the MIT License which + * is available at https://opensource.org/license/MIT. + * + * SPDX-License-Identifier: MIT + */ + +import * as Blockly from 'blockly' + +// constrains for number of lights and number of switch points +const light_constrains = [0, 23] +const switch_constrains = [0, 29] + +// Map of parent block type to constraint range +const parentConstraints = { + PointStatement: switch_constrains, + LightStatement: light_constrains +} +const defaultConstraints = [-Infinity, Infinity] // no constraint by default + +function getConstraintsFor (block) { + const parentType = block.parentBlock_?.type + return parentConstraints[parentType] || defaultConstraints +} + +// Reusable validator factory +function makeRangeValidator (block) { + return function (newValue) { + const [min, max] = getConstraintsFor(block) + const value = Number(newValue) + + if (value < min || value > max) { + return String(Math.min(Math.max(value, min), max)) // clamp into range + } + + return newValue + } +} + +function revalidateField (block, name) { + const field = block.getField(name) + if (field) { + // Re-running setValue with the field's own current value forces the validator to execute again + field.setValue(field.getValue()) + } +} + + +// validator for int_number +Blockly.Extensions.register('dynamic_int_number_validator', function () { + const field = this.getField('NUM') + if (field) { + field.setValidator(makeRangeValidator(this)) + } +}) + + +// validator for int_range +Blockly.Extensions.register('dynamic_int_range_validator', function () { + const block = this + ;['START', 'END'].forEach(name => { + const field = block.getField(name) + if (field) { + field.setValidator(makeRangeValidator(block)) + } + }) + + // when block is moved into a new parent, re-evaluate + block.workspace.addChangeListener(function (event) { + if (event.type !== Blockly.Events.BLOCK_MOVE) return + ;['START', 'END'].forEach(name => revalidateField(block, name)) + }) +}) diff --git a/locales/de/de_tokens.js b/locales/de/de_tokens.js index 5db76f4..bc60b17 100644 --- a/locales/de/de_tokens.js +++ b/locales/de/de_tokens.js @@ -39,8 +39,8 @@ export const de_tokens = { RAILBLOCKS_CONTACT_WAIT_FIRST: 'erster', RAILBLOCKS_CONTACT_WAIT_SECOND: 'zweiter', - RAILBLOCKS_NUMBER_RANGE_TEXT: '%1 bis %2', - RAILBLOCKS_NUMBER_RANGE_TOOLTIP: 'Erstellt eine Reihe von Nummern.', + RAILBLOCKS_INT_RANGE_TEXT: '%1 bis %2', + RAILBLOCKS_INT_RANGE_TOOLTIP: 'Erstellt eine Reihe von Nummern.', RAILBLOCKS_TIME_WAIT_TEXT: 'Warte für %1 Sekunde(n)', RAILBLOCKS_TIME_WAIT_TOOLTIP: 'Tue nichts für eine bestimmte Zeit.', @@ -83,6 +83,5 @@ export const de_tokens = { RAILBLOCKS_WARNING_UNREACHABLE_WEAK: 'Blöcke nach diesem können vielleicht nicht erreicht werden, da sich eine Wiederholung innerhalb dieses Blocks befindet.', RAILBLOCKS_WARNING_EMPTY_INPUT: 'Dieser Block hat leere Felder und wird einen Syntaxfehler verursachen!', RAILBLOCKS_WARNING_UNUSED: 'Unbenutzter Block', - RAILBLOCKS_WARNING_BROKEN_CONSTRAIN: 'Dieser Block enthält Zahlen, welche außerhalb des gültigen Bereichs liegen.', RAILBLOCKS_WARNING_PASSED: 'Der "passiert" Block kann momentan nicht in diesem Block genutzt werden.', } diff --git a/locales/en/en_tokens.js b/locales/en/en_tokens.js index 4c0696a..7cd26d7 100644 --- a/locales/en/en_tokens.js +++ b/locales/en/en_tokens.js @@ -39,8 +39,8 @@ export const en_tokens = { RAILBLOCKS_CONTACT_WAIT_FIRST: 'first', RAILBLOCKS_CONTACT_WAIT_SECOND: 'second', - RAILBLOCKS_NUMBER_RANGE_TEXT: '%1 to %2', - RAILBLOCKS_NUMBER_RANGE_TOOLTIP: 'Creates a range of numbers.', + RAILBLOCKS_INT_RANGE_TEXT: '%1 to %2', + RAILBLOCKS_INT_RANGE_TOOLTIP: 'Creates a range of numbers.', RAILBLOCKS_TIME_WAIT_TEXT: 'Wait for %1 second(s)', RAILBLOCKS_TIME_WAIT_TOOLTIP: 'Do nothing for a certain amount of time.', @@ -83,6 +83,5 @@ export const en_tokens = { RAILBLOCKS_WARNING_UNREACHABLE_WEAK: 'Blocks after this may not be reached because of a loop inside this.', RAILBLOCKS_WARNING_EMPTY_INPUT: 'This block has empty inputs and will cause a syntax error!', RAILBLOCKS_WARNING_UNUSED: 'Unused block', - RAILBLOCKS_WARNING_BROKEN_CONSTRAIN: 'This block has numbers that are outside the valid range.', RAILBLOCKS_WARNING_PASSED: 'The "passed" block can currently not be used in this block.', } From 3deede35ff4011a6de297fe79575da794f2c44e8 Mon Sep 17 00:00:00 2001 From: Tokessa Hamann Date: Sun, 2 Aug 2026 14:22:36 +0200 Subject: [PATCH 6/7] constraints --- frontend/validators.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/validators.js b/frontend/validators.js index 41d1975..4ec15cb 100644 --- a/frontend/validators.js +++ b/frontend/validators.js @@ -19,14 +19,14 @@ import * as Blockly from 'blockly' -// constrains for number of lights and number of switch points -const light_constrains = [0, 23] -const switch_constrains = [0, 29] +// constraints for number of lights and number of switch points +const light_constraints = [0, 23] +const switch_constraints = [0, 29] // Map of parent block type to constraint range const parentConstraints = { - PointStatement: switch_constrains, - LightStatement: light_constrains + PointStatement: switch_constraints, + LightStatement: light_constraints } const defaultConstraints = [-Infinity, Infinity] // no constraint by default From 9348e177716153484064377c62a2a3112579a44b Mon Sep 17 00:00:00 2001 From: Tokessa Hamann Date: Tue, 4 Aug 2026 13:35:21 +0200 Subject: [PATCH 7/7] small update due to pr --- frontend/dynamic_blocks.js | 8 +++----- locales/de/de_tokens.js | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/dynamic_blocks.js b/frontend/dynamic_blocks.js index 11f8182..cf4eb54 100644 --- a/frontend/dynamic_blocks.js +++ b/frontend/dynamic_blocks.js @@ -90,9 +90,9 @@ function buildAnotherCondRow(index, block){ const input = block.appendValueInput(`COND_BOOL${index}`) .setCheck('Boolean') - // if (index !== 0) { - // input.appendField(new Blockly.FieldLabel(' ')) // empty placeholder - // } + if (index !== 0) { + input.appendField(new Blockly.FieldLabel(' ')) // empty placeholder + } input .appendField(Blockly.Msg.RAILBLOCKS_CONDITIONAL_TEXT_START) @@ -328,7 +328,6 @@ Blockly.Blocks.PointStatement = { domToMutation, updateShape: function () { - // get current status for recreation after +- const branchOption = this.getFieldValue('BRANCH_OPTION') let currentCount = 0 @@ -394,7 +393,6 @@ Blockly.Blocks.LightStatement = { domToMutation, updateShape: function () { - // get current status for recreation after +- const lightStatus = this.getFieldValue('LIGHT_STATUS') let currentCount = 0 diff --git a/locales/de/de_tokens.js b/locales/de/de_tokens.js index bc60b17..fede577 100644 --- a/locales/de/de_tokens.js +++ b/locales/de/de_tokens.js @@ -40,7 +40,7 @@ export const de_tokens = { RAILBLOCKS_CONTACT_WAIT_SECOND: 'zweiter', RAILBLOCKS_INT_RANGE_TEXT: '%1 bis %2', - RAILBLOCKS_INT_RANGE_TOOLTIP: 'Erstellt eine Reihe von Nummern.', + RAILBLOCKS_INT_RANGE_TOOLTIP: 'Erstellt eine Reihe von Zahlen.', RAILBLOCKS_TIME_WAIT_TEXT: 'Warte für %1 Sekunde(n)', RAILBLOCKS_TIME_WAIT_TOOLTIP: 'Tue nichts für eine bestimmte Zeit.',