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

This commit is contained in:
feie9454 2025-09-12 11:17:29 +08:00
parent 09e6ff720e
commit 9a7b4effba
4 changed files with 172 additions and 19 deletions

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->CAB->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

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 },
],
},
{
@ -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`' },
],
},
]