diff --git a/src/App.vue b/src/App.vue index 647f668..e11a684 100644 --- a/src/App.vue +++ b/src/App.vue @@ -169,6 +169,8 @@ function onWheel(e: WheelEvent) { let draggingId: string | null = null let dragOffset = { x: 0, y: 0 } // 元素内相对偏移(世界坐标) const mouseWorld = reactive({ x: 0, y: 0 }) +// 滑动变阻器滑块拖动状态 +let sliderDraggingId: string | null = null function onInstanceMouseDown(e: MouseEvent, inst: Instance) { if (e.button !== 0) return @@ -191,6 +193,37 @@ function onViewportMouseUpDrag() { draggingId = null } +// 将世界坐标转换为实例局部坐标(考虑旋转) +function worldToLocal(inst: Instance, wx: number, wy: number) { + const dx = wx - inst.x + const dy = wy - inst.y + const rad = (inst.rotation || 0) * Math.PI / 180 + const cos = Math.cos(rad) + const sin = Math.sin(rad) + // 逆旋转 + const lx = dx * cos + dy * sin + const ly = -dx * sin + dy * cos + return { lx, ly } +} + +function onViewportMouseMoveSlider(e: MouseEvent) { + if (!sliderDraggingId) return + const inst = instances.find(i => i.id === sliderDraggingId) + if (!inst) return + if (inst.key !== 'sliding_rheostat') return + const { x: wx, y: wy } = clientToWorld(e.clientX, e.clientY) + const { lx } = worldToLocal(inst, wx, wy) + // 滑道范围(与 getRheostatPinOffset 对齐) + const leftFrac = 0.25, rightFrac = 0.75 + const t = (lx / inst.size + 0.5 - leftFrac) / (rightFrac - leftFrac) + const pos = Math.max(0, Math.min(1, t)) + inst.props['position'] = pos +} + +function onViewportMouseUpSlider() { + sliderDraggingId = null +} + // 连接点点击逻辑 function onEndpointClick(instId: string, cpIndex: number, e: MouseEvent) { e.stopPropagation() @@ -326,6 +359,15 @@ function getElemMeta(inst: Instance) { return elements.find(e => e.key === inst.key) } +// 针对滑动变阻器:计算滑块的屏幕内偏移(未考虑旋转,先期简化为只支持0°/180°朝向的水平滑动) +function getRheostatPinOffset(inst: Instance) { + const pos = Math.max(0, Math.min(1, Number(inst.props?.['position'] ?? 0.5))) + + const leftFrac = 0.25, rightFrac = 0.75 + const cx = (leftFrac + (rightFrac - leftFrac) * pos) * inst.size + return cx +} + // 并查集用于将端点连成节点 class DSU { parent = new Map() @@ -382,9 +424,16 @@ function buildNet(): BuiltNet { // 形成节点集合 const roots = new Set() for (const t of allTerms) roots.add(dsu.find(t)) + // 仅基于真实端点选择参考地 const nodeIds = Array.from(roots) const ground = nodeIds.length ? nodeIds[0] : null // 任取一个参考地 + // 为后续引入的“内部节点”(例如串联内阻的中间点)保留集合,避免重复 + const nodeSet = new Set(nodeIds) + const addNode = (nid: string) => { + if (!nodeSet.has(nid)) { nodeSet.add(nid); nodeIds.push(nid) } + } + // 分类器件 const resistors: BuiltNet['resistors'] = [] const vsrcs: BuiltNet['vsrcs'] = [] @@ -400,7 +449,20 @@ function buildNet(): BuiltNet { const key = meta.key if (key === 'battery') { const V = safeNum(inst.props?.['voltage'], 0) - vsrcs.push({ inst, nPlus: n0, nMinus: n1, V }) + const Rint = safeNum(inst.props?.['internalResistance'], 0.1) + // 用内部节点将电压源与内阻串联:n0 --[V]--> nInt --[Rint]--> n1 + const nInt = `${inst.id}:int` + addNode(nInt) + vsrcs.push({ inst, nPlus: n0, nMinus: nInt, V }) + resistors.push({ inst, nPlus: nInt, nMinus: n1, R: Math.max(1e-9, Rint) }) + } else if (key === 'power_supply') { + const V = safeNum(inst.props?.['voltage'], 0) + const Rint = safeNum(inst.props?.['internalResistance'], 0.1) + // 用内部节点将电压源与内阻串联:n0 --[V]--> nInt --[Rint]--> n1 + const nInt = `${inst.id}:int` + addNode(nInt) + vsrcs.push({ inst, nPlus: n0, nMinus: nInt, V }) + resistors.push({ inst, nPlus: nInt, nMinus: n1, R: Math.max(1e-9, Rint) }) } else if (key === 'switch') { if (inst.state === 'on') { vsrcs.push({ inst, nPlus: n0, nMinus: n1, V: 0 }) @@ -412,6 +474,22 @@ function buildNet(): BuiltNet { } else if (key === 'capacitor') { // DC 断开 // 忽略 + } else if (key === 'sliding_rheostat') { + // 四脚:A(0) B(1) C(2) D(3) + const n2 = getNode(inst, 2) + const n3 = getNode(inst, 3) + // AB 直通:用 0V 电压源建模 + vsrcs.push({ inst, nPlus: n0, nMinus: n1, V: 0 }) + // 变量电阻:AB->C、AB->D 与滑块位置有关 + const Rmax = Math.max(1e-6, safeNum(inst.props?.['maxResistance'], 100)) + const pos = Math.max(0, Math.min(1, safeNum(inst.props?.['position'], 0.5))) + const Rac = Math.max(1e-6, Rmax * pos) + const Rad = Math.max(1e-6, Rmax * (1 - pos)) + // 将 AB 汇成一个等效节点(任选 A 侧 n0)到 C/D + resistors.push({ inst, nPlus: n0, nMinus: n2, R: Rac }) + resistors.push({ inst, nPlus: n0, nMinus: n3, R: Rad }) + // CD 固定为最大电阻 + resistors.push({ inst, nPlus: n2, nMinus: n3, R: Rmax }) } else if (key === 'resistor' || key === 'light_bulb' || key === 'meter') { const R = Math.max(1e-6, safeNum(inst.props?.['resistance'], 0)) resistors.push({ inst, nPlus: n0, nMinus: n1, R }) @@ -570,7 +648,6 @@ function meterText(inst: Instance) { if (!code) return `${(i).toFixed(6)} A` try { // 允许两种形态:"(i)=>..." 或 "(i,v,props)=>..." - // 使用字符串拼接避免模板字符串与用户代码中的反引号冲突 const fn = new Function('i', 'r', 'props', 'return (' + code + ')(i, r, props)') const out = fn(i, r, inst.props) return String(out) @@ -605,9 +682,6 @@ function centerWorld() { onMounted(() => { centerWorld() - // 处理窗口缩放自适应居中(不覆盖用户拖动,首次即可) - const ro = new ResizeObserver(() => centerWorld()) - if (viewportRef.value) ro.observe(viewportRef.value) }) // 9) 导出 / 导入(JSON 序列化) @@ -878,17 +952,16 @@ function deleteSelected() {
+ @mousemove="(e) => { onViewportMouseMove(e); onViewportMouseMoveDrag(e); onViewportMouseMoveTrack(e); onViewportMouseMoveSlider(e) }" + @mouseup="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" + @mouseleave="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" @wheel="onWheel" + @dragover="onCanvasDragOver" @drop="onCanvasDrop" @click="clearSelection">
- + + + pin +
{{ meterText(inst) }}
+ +
+ {{ instLive[inst.id].v?.toFixed(2) }} V + {{ instLive[inst.id].i?.toFixed(2) }} A +
@@ -1258,6 +1342,17 @@ function deleteSelected() { image-rendering: -webkit-optimize-contrast; } +.rheo-pin { + position: absolute; + z-index: 3; + cursor: ew-resize; + pointer-events: auto; + width: 15% !important; + height: auto !important; + top: 25.5% !important; + transform: translateX(-50%); +} + .bulb-glow { position: absolute; left: 0; @@ -1284,6 +1379,23 @@ function deleteSelected() { pointer-events: none; } +.power-supply-label { + position: absolute; + right: 44%; + height: 38%; + top: 17%; + color: #62D3EF; + text-shadow: 0 0 2px #62D3EF, 0 0 6px #62d3ef44; + font-family: "meter-font"; + font-size: 22px; + white-space: nowrap; + pointer-events: none; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: space-between; +} + .endpoint { position: absolute; width: 14px; diff --git a/src/circuit-ele/power_supply.png b/src/circuit-ele/power_supply.png new file mode 100644 index 0000000..d6dce83 Binary files /dev/null and b/src/circuit-ele/power_supply.png differ diff --git a/src/circuit-ele/sliding_rheostat_pin.png b/src/circuit-ele/sliding_rheostat_pin.png index da788d7..490a29e 100644 Binary files a/src/circuit-ele/sliding_rheostat_pin.png and b/src/circuit-ele/sliding_rheostat_pin.png differ diff --git a/src/elements.ts b/src/elements.ts index a9e32c1..3a83dc9 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -1,6 +1,7 @@ // 显式导入所有电路元件图片,并定义其基本属性 import battery from './circuit-ele/battery.png' +import powerSupply from './circuit-ele/power_supply.png' import capacitor from './circuit-ele/capacitor.png' import inductor from './circuit-ele/inductor.png' import lightBulb from './circuit-ele/light_bulb.png' @@ -9,6 +10,8 @@ import resistor from './circuit-ele/resistor.png' import switchOff from './circuit-ele/switch_off.png' import switchOn from './circuit-ele/switch_on.png' import meter from './circuit-ele/meter.png' +import slidingRheostat from './circuit-ele/sliding_rheostat.png' +import slidingRheostatPin from './circuit-ele/sliding_rheostat_pin.png' export type ConnectionPoint = { // 以百分比(0..1)定义在图片盒子内的位置 @@ -27,6 +30,8 @@ export type CircuitElement = { stateImages?: { on: string; off: string } // 预设:一键应用一组属性值 preset?: Preset[] + // 可选:叠加图片(例如滑动变阻器的滑块) + pinUrl?: string } export type PropertySchema = { @@ -53,8 +58,23 @@ export const elements: CircuitElement[] = [ { x: 0.05, y: 0.5, name: 'A' }, // 左侧 { x: 0.95, y: 0.5, name: 'B' }, // 右侧 ], + propertySchemas: [ + { key: 'voltage', label: '电压', type: 'number', unit: 'V', default: 3 }, + { key: 'internalResistance', label: '内阻', type: 'number', unit: 'Ω', default: 0.5 }, + ], + }, + { + key: 'power_supply', + name: '电源', + url: powerSupply, + defaultSize: 180, + connectionPoints: [ + { x: 0.2, y: 1, name: 'A' }, + { x: 0.8, y: 1, name: 'B' }, + ], propertySchemas: [ { key: 'voltage', label: '电压', type: 'number', unit: 'V', default: 5 }, + { key: 'internalResistance', label: '内阻', type: 'number', unit: 'Ω', default: 0.1 }, ], }, { @@ -82,8 +102,8 @@ export const elements: CircuitElement[] = [ propertySchemas: [ { key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 20 }, ], - // 当电流超过阈值时在前端根据 state 切换图片 - stateImages: { on: lightBulbGrow, off: lightBulb }, + // 当电流超过阈值时在前端根据 state 切换图片 + stateImages: { on: lightBulbGrow, off: lightBulb }, }, { key: 'capacitor', @@ -111,6 +131,23 @@ export const elements: CircuitElement[] = [ { key: 'inductance', label: '电感', type: 'number', unit: 'H', default: 0.001 }, ], }, + { + key: 'sliding_rheostat', + name: '滑动变阻器', + url: slidingRheostat, + pinUrl: slidingRheostatPin, + defaultSize: 180, + connectionPoints: [ + { x: 0.15, y: 0.28, name: 'A' }, + { x: 0.85, y: 0.28, name: 'B' }, + { x: 0.15, y: 0.76, name: 'C' }, + { x: 0.85, y: 0.76, name: 'D' }, + ], + propertySchemas: [ + { key: 'maxResistance', label: '最大电阻', type: 'number', unit: 'Ω', default: 100 }, + { key: 'position', label: '滑块位置', type: 'number', default: 0.5 }, + ], + }, { key: 'resistor', name: '电阻', @@ -133,14 +170,18 @@ export const elements: CircuitElement[] = [ { x: 0.25, y: 1, name: 'A' }, { x: 0.45, y: 1, name: 'B' }, ], - preset:[{ - name: '电流表', propertyValues: { resistance: 1, renderFunc: '(i) => `${(i * 1000).toFixed(2)} mA`' } - },{ - name: '电压表', propertyValues: { resistance: 1000000, renderFunc: '(i, r) => `${(i * r).toFixed(2)} V`' } + preset: [{ + name: '微安电流表', propertyValues: { resistance: 0.05, renderFunc: '(i) => `${(i * 1000000).toFixed(0)} uA`' } + }, { + name: '毫安电流表', propertyValues: { resistance: 0.5, renderFunc: '(i) => `${(i * 1000).toFixed(1)} mA`' } + }, { + name: '电流表', propertyValues: { resistance: 0.1, renderFunc: '(i) => `${(i).toFixed(2)} A`' } + }, { + name: '电压表', propertyValues: { resistance: 10000000, renderFunc: '(i, r) => `${(i * r).toFixed(2)} V`' } }], propertySchemas: [ - { key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 1 }, - { key: 'renderFunc', label: '显示函数', type: 'text', default: '(i) => `${(i * 1000).toFixed(2)} mA`' }, + { key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 0.1 }, + { key: 'renderFunc', label: '显示函数', type: 'text', default: '(i) => `${(i).toFixed(2)} A`' }, ], }, ]