增加滑动变阻器和可调电压

This commit is contained in:
2025-09-12 11:17:29 +08:00
parent 09e6ff720e
commit 9a7b4effba
4 changed files with 172 additions and 19 deletions
+123 -11
View File
@@ -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<string, string>()
@@ -382,9 +424,16 @@ function buildNet(): BuiltNet {
// 形成节点集合
const roots = new Set<string>()
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() {
<!-- 画布视口(可平移、缩放、接收拖拽) -->
<main class="viewport" ref="viewportRef" @mousedown="onViewportMouseDown"
@mousemove="(e) => { onViewportMouseMove(e); onViewportMouseMoveDrag(e); onViewportMouseMoveTrack(e) }"
@mouseup="() => { onViewportMouseUp(); onViewportMouseUpDrag() }"
@mouseleave="() => { onViewportMouseUp(); onViewportMouseUpDrag() }" @wheel="onWheel" @dragover="onCanvasDragOver"
@drop="onCanvasDrop" @click="clearSelection">
@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">
<!-- 世界平面:网格背景,应用 transform -->
<div class="world" :style="worldStyle" ref="worldRef">
<!-- 导线(SVG 覆盖层) -->
<svg class="wires" :width="state.worldSize" :height="state.worldSize" @mousemove="onViewportMouseMoveTrack">
<g>
<g v-for="w in wires" :key="w.id" class="wire-group"
@click.stop="(e) => removeWire(w.id, e)">
<g v-for="w in wires" :key="w.id" class="wire-group" @click.stop="(e) => removeWire(w.id, e)">
<line class="wire-base" :x1="getConnectionPointWorldPos(getInstance(w.a.instId)!, w.a.cpIndex).x"
:y1="getConnectionPointWorldPos(getInstance(w.a.instId)!, w.a.cpIndex).y"
:x2="getConnectionPointWorldPos(getInstance(w.b.instId)!, w.b.cpIndex).x"
@@ -915,6 +988,12 @@ function deleteSelected() {
:alt="inst.key" :draggable="false"
@contextmenu.prevent="() => { const meta = elements.find(e => e.key === inst.key); if (meta?.stateImages) inst.state = inst.state === 'on' ? 'off' : 'on' }" />
<!-- 滑动变阻器滑块 -->
<img v-if="inst.key === 'sliding_rheostat'" class="rheo-pin"
:src="(elements.find(e => e.key === inst.key) as any)?.pinUrl" alt="pin" draggable="false" :style="{
left: ((getRheostatPinOffset(inst) / inst.size) * 100) + '%',
}" @mousedown.stop="() => { sliderDraggingId = inst.id }" />
<!-- 灯泡发散光线叠加层 -->
<svg v-if="inst.key === 'light_bulb'" class="bulb-glow" :width="inst.size" :height="inst.size"
:viewBox="'0 0 ' + inst.size + ' ' + inst.size" aria-hidden="true">
@@ -932,6 +1011,11 @@ function deleteSelected() {
<div v-if="inst.key === 'meter'" class="meter-label">
{{ meterText(inst) }}
</div>
<div v-if="inst.key === 'power_supply'" class="power-supply-label">
<span>{{ instLive[inst.id].v?.toFixed(2) }} V</span>
<span>{{ instLive[inst.id].i?.toFixed(2) }} A</span>
</div>
</div>
</div>
@@ -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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 37 KiB

+47 -6
View File
@@ -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 },
],
},
{
@@ -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`' },
],
},
]