从零到一:用JointJS复刻一个简易的“逻辑门”模拟器(含完整源码)
从零构建逻辑门模拟器:JointJS实战与可视化电路设计
在数字电路设计与计算机科学教育中,逻辑门是最基础的构建模块。传统教学往往依赖抽象符号或静态图示,而通过JointJS构建交互式逻辑门模拟器,不仅能直观展示与门、或门、非门的工作原理,还能让学习者通过拖拽连接亲自搭建电路。本文将完整演示如何利用JointJS的joint.shapes.devs.Model创建带端口的可交互逻辑门元件,实现信号传递的动态模拟,并最终导出为可持久化的JSON数据。
1. 环境准备与JointJS核心概念
1.1 技术栈选型分析
JointJS作为基于SVG的图形库,其核心优势在于:
- 元素-连接模型:所有图形由
Element和Link两类对象构成 - 端口系统:通过
ports定义输入输出接口 - 事件驱动:支持拖拽、连线、点击等交互事件的监听处理
- 序列化能力:整个图形可导出为JSON格式
与其他可视化库(如D3.js或GoJS)相比,JointJS特别适合需要复杂连接关系的场景。下表对比了常见图形库特性:
| 特性 | JointJS | D3.js | GoJS |
|---|---|---|---|
| 预置图形元素 | ✔ | ✖ | ✔ |
| 连接线管理 | ✔ | ✖ | ✔ |
| 端口系统 | ✔ | ✖ | ✔ |
| 序列化支持 | ✔ | ✖ | ✔ |
| 学习曲线 | 中等 | 陡峭 | 平缓 |
1.2 项目初始化
通过npm安装依赖:
npm install jointjs jquery backbone lodash基础HTML结构:
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="node_modules/jointjs/dist/joint.min.css"> </head> <body> <div id="canvas"></div> <script src="node_modules/jquery/dist/jquery.min.js"></script> <script src="node_modules/lodash/lodash.min.js"></script> <script src="node_modules/backbone/backbone-min.js"></script> <script src="node_modules/jointjs/dist/joint.min.js"></script> <script src="app.js"></script> </body> </html>2. 构建逻辑门元件库
2.1 定义基础元件模板
使用joint.shapes.devs.Model创建可复用的逻辑门模板:
const gateTemplate = new joint.shapes.devs.Model({ size: { width: 60, height: 80 }, ports: { groups: { 'in': { attrs: { '.port-body': { magnet: 'passive', width: 10, height: 10 } }, position: { name: 'left' } }, 'out': { attrs: { '.port-body': { magnet: 'active', width: 10, height: 10 } }, position: { name: 'right' } } } }, attrs: { '.body': { stroke: '#333', fill: '#FFF' }, '.label': { text: 'GATE', fontSize: 12 } } });2.2 实现具体逻辑门类型
基于模板创建三种基本逻辑门:
与门(AND)实现:
function createANDGate(x, y) { return gateTemplate.clone().set({ position: { x, y }, inPorts: ['A', 'B'], outPorts: ['Q'], attrs: { '.label': { text: 'AND' }, '.body': { d: 'M 0 0 L 60 0 L 60 80 L 30 80 L 0 40 Z' } } }); }或门(OR)实现:
function createORGate(x, y) { return gateTemplate.clone().set({ position: { x, y }, inPorts: ['A', 'B'], outPorts: ['Q'], attrs: { '.label': { text: 'OR' }, '.body': { d: 'M 0 20 Q 30 -20 60 20 L 60 60 Q 30 100 0 60 Z' } } }); }非门(NOT)实现:
function createNOTGate(x, y) { return gateTemplate.clone().set({ position: { x, y }, inPorts: ['A'], outPorts: ['Q'], attrs: { '.label': { text: 'NOT' }, '.body': { d: 'M 0 20 L 40 20 L 60 40 L 40 60 L 0 60 Z' } } }); }3. 信号传递与交互逻辑
3.1 连接线行为配置
设置连线样式与交互规则:
const paper = new joint.dia.Paper({ el: document.getElementById('canvas'), model: graph, width: 800, height: 600, defaultLink: new joint.shapes.standard.Link({ attrs: { line: { stroke: '#555', strokeWidth: 2, targetMarker: { 'type': 'path', 'd': 'M 10 -5 0 0 10 5 Z' } } }, router: { name: 'manhattan' } }), validateConnection: function(cellViewS, magnetS, cellViewT, magnetT) { // 只允许输出端口连接到输入端口 return magnetS.getAttribute('port-group') === 'out' && magnetT.getAttribute('port-group') === 'in'; } });3.2 实时信号计算
实现逻辑门运算核心逻辑:
function calculateOutput(gate, inputValues) { switch(gate.attr('.label/text')) { case 'AND': return inputValues.every(v => v === 1) ? 1 : 0; case 'OR': return inputValues.some(v => v === 1) ? 1 : 0; case 'NOT': return inputValues[0] === 1 ? 0 : 1; default: return 0; } } // 更新所有连接元件状态 function updateCircuit() { graph.getElements().forEach(gate => { const inputs = graph.getConnectedLinks(gate, { inbound: true }); const inputValues = inputs.map(link => link.get('source').value || 0); const outputValue = calculateOutput(gate, inputValues); // 更新输出端口值 gate.set('outValue', outputValue); // 传播到下游元件 graph.getConnectedLinks(gate, { outbound: true }).forEach(link => { link.set('target').value = outputValue; link.attr('line/stroke', outputValue ? '#F00' : '#555'); }); }); }4. 高级功能实现
4.1 状态持久化与恢复
导出当前电路为JSON:
function exportCircuit() { return JSON.stringify(graph.toJSON()); }从JSON导入电路:
function importCircuit(json) { graph.clear(); graph.fromJSON(JSON.parse(json)); }4.2 动态元件创建界面
创建可拖拽的元件面板:
const palette = new joint.ui.Palette({ el: document.getElementById('palette'), groups: { gates: { label: '逻辑门', index: 1 } }, groupsCollapsible: true }); palette.addGroup('gates', [ { type: 'AND', template: createANDGate(0, 0), label: '与门(AND)' }, { type: 'OR', template: createORGate(0, 0), label: '或门(OR)' }, { type: 'NOT', template: createNOTGate(0, 0), label: '非门(NOT)' } ]); // 启用拖拽创建 palette.on('element:dragstart', function(elementView) { elementView.model.position(0, 0); // 重置位置 });5. 教学应用场景扩展
5.1 真值表可视化
自动生成当前电路真值表:
function generateTruthTable() { const inputs = []; const outputs = []; // 识别所有输入输出端口 graph.getElements().forEach(gate => { if (gate.get('inPorts').length > 0 && graph.getConnectedLinks(gate, { inbound: true }).length === 0) { inputs.push(gate); } if (gate.get('outPorts').length > 0 && graph.getConnectedLinks(gate, { outbound: true }).length === 0) { outputs.push(gate); } }); // 生成所有可能的输入组合 const combinations = Math.pow(2, inputs.length); const table = []; for (let i = 0; i < combinations; i++) { const row = {}; // 设置输入值 inputs.forEach((input, idx) => { const value = (i >> idx) & 1; input.set('inValue', value); row[input.attr('.label/text')] = value; }); // 计算输出 updateCircuit(); // 记录输出值 outputs.forEach(output => { row[output.attr('.label/text')] = output.get('outValue'); }); table.push(row); } return table; }5.2 电路验证模式
添加自动验证功能:
function verifyCircuit(expectedLogic) { const truthTable = generateTruthTable(); return truthTable.every(row => { const inputs = Object.keys(row) .filter(k => ['AND','OR','NOT'].includes(k)) .map(k => row[k]); const output = row['OUTPUT']; return expectedLogic(inputs) === output; }); } // 示例:验证一个AND-OR组合电路 const isValid = verifyCircuit(inputs => { return (inputs[0] && inputs[1]) || inputs[2]; });6. 性能优化与实践建议
6.1 大规模电路处理
当元件数量超过100个时,可采取以下优化措施:
- 批量操作:使用
graph.startBatch()和graph.stopBatch()包裹多次更新 - 虚拟渲染:对不可见区域启用延迟渲染
- 简化样式:减少复杂SVG滤镜和渐变的使用
// 批量操作示例 graph.startBatch('complex_update'); // 执行多个元素修改 elements.forEach(el => el.set('position', randomPosition())); graph.stopBatch('complex_update');6.2 调试技巧
常见问题排查方法:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 连线无法连接 | 端口magnet属性未正确设置 | 检查port-group和magnet配置 |
| 元素显示不全 | 画布尺寸不足 | 调用paper.scaleContentToFit() |
| 性能明显下降 | 频繁触发全局更新 | 使用增量更新代替全量计算 |
| 导入后图形错位 | 坐标系统不一致 | 检查JSON中的position单位 |
6.3 扩展方向
基于当前项目可进一步开发:
- 复合逻辑门:将常用电路组合保存为可复用的自定义元件
- 时序电路:添加时钟信号支持触发器、计数器等元件
- 教学关卡:设计渐进式电路搭建挑战任务
- 多人协作:通过WebSocket实现实时协作编辑
// 自定义复合元件示例 joint.shapes.logic.CompoundGate = joint.shapes.devs.Model.extend({ defaults: joint.util.deepSupplement({ type: 'logic.CompoundGate', // 自定义属性和端口 }, joint.shapes.devs.Model.prototype.defaults) });在开发过程中,特别注意保持代码模块化,将图形定义、业务逻辑和交互控制分离。例如,可以将所有元件模板集中在components.js中,电路计算逻辑放在simulator.js中,而界面交互则通过controller.js来管理。
