增加滑动变阻器和可调电压
This commit is contained in:
parent
09e6ff720e
commit
9a7b4effba
134
src/App.vue
134
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<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;
|
||||
|
||||
BIN
src/circuit-ele/power_supply.png
Normal file
BIN
src/circuit-ele/power_supply.png
Normal file
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 |
@ -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`' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user