- resources path process.resourcesPath
修复物理引擎的一个 bug
最近遇到一个很奇怪的问题
我的物理引擎,总是在稳定下来之后一段时间内发生爆炸,效果如下 
简单梳理下我的物理引擎的处理流程
- 通过
GJK算法分析是否碰撞 - 通过
EPA算法得到碰撞法线和碰撞深度 - 通过
v_clip算法得到足够多的碰撞点对 - 约束
经过排查发现,是EPA 的过程出错了(EPA 算法解析),如果一个Minkowski边已经在原点上了, 那么理论上是无法expand的
所以一开始想到的解决方案是,判断 Minkowski 边是否在原点上,或者非常靠近原点,但是无论怎么调整参数,都不行,原因是:
根本无法精准的判定Minkowski 边是否在原点上,计算偏差总是存在的
- 如果误判在原点上,那么最终结果肯定是错的
- 如果误判不在原点上,继续扩展,因为该边足够靠近原点,得到的指向原点的
向量的方向很可能是相反的,该算法就会继续扩展,得到错误的结果
但是理性下来分析,为什么是稳定了一段时间后才会出现这个问题呢?
因为随着物理引擎的约束(持续冲量),系统会不断的逼近稳定的状态,一开始系统内物体之间的碰撞有很大的碰撞深度,
随着系统的持续约束,物体之间的碰撞深度不断的减少,最后逼近 0。这个时候,Minkowski最靠近原点的边就会落在原点上,然后导致计算的偏差,
要想解决这个问题,可以让物体之前保持一个最小的碰撞深度,比如 0.001, 这样就不会出现Minkowski最靠近原点的边落在原点上的情况了
代码如下
_10let permeate = contact_info.depth - constraint_parameters.max_allow_permeate;
最后的效果如下(不考虑摩擦力)

现在系统就很稳定了
event-ts
3 个使用 @swnb/event 的理由
1. 简化 web 事件监听模型
在 react 里面, 有时候你不得不这么写来处理监听事件
如果你想要处理多个事件
这太难受了
如果你使用 @swnb/event
编程从来没有这么简单过
2. Promise support
考虑一个场景, 你要建立一个 websocket 连接, 并等待连接打开, 并设置最大连接时长, 考虑正确的释放资源, 你可能会写如下的代码
实在是太烦人了
如果你使用 @swnb/event
编程从来没有这么简单过
考虑一个更加复杂的场景, 创建一个 webrtc 连接, 等待连接 connected
使用 where 可以选择你想要的那个 connectionState
3. 观察一个 web 对象的所有事件
如果你想知道 video 在播放的时候触发了哪些事件, 可以考虑这么写
放在 react 里面可以这么写
打开控制台, 你会看到所有 video 的事件触发顺序和时间

编程从来没有这么简单过
typescript 配置
tsconfig.json 的配置很多,大致内容如下
大致说一下一些难以理解的配置项
esModuleInterop
通常来说 fsDefault 和 fsNamespace 是不同的,如果你打开了 esModuleInterop, 那么 fsDefault 和 fsNamespace 就是相同的,如果你碰到 import fs from 'fs' 报错的情况
那么应该是改配置没有打开的原因
noEmit
该配置会导致 tsc 不输出任何文件,一般当你需要用 tsc 只做类型检查,不生成代码的时候,你可以考虑打开该配置
include , files rootDir and outDir
如果使用 files 指定所有 ts 文件是很麻烦的
直接使用 "include":["src"] 可以涵盖该目录的所有文件
rootDir 指定输出代码所在的目录树的顶部 The compilerOptions.rootDir option defines the root of the tree at outDir , 默认如果文件在 rootDir 不在 include 会有一些错误提示
moduleResolution
查找文件的方式,建议就是 node, 规定了 import 查找文件的方式
paths
paths 和 baseUrl 指定了在 import 的时候时候如何查找文件,但是它不负责帮你转换成相对路径,所以如果你在使用 tsc ,并将它用于库的时候,请使用 tsc-alias 或者禁用 baseUrl 属性
浏览器的任务队列和渲染
看看下面的代码输出什么
如果你把上面的代码复制到浏览器的控制台, 输出结果应该是2 1 , 为什么?
按照常规的宏任务和渲染的理解, requestAnimationFrame 和 setTimeout 都是宏任务, 浏览器的执行顺序是 requestAnimationFrame -> 渲染(render) -> setTimeout, 输出的结果应该是 1 2 才对
这就要说到一个重点, 那就是宏任务和宏任务之间不一定会发生渲染, 浏览器会判断是否需要渲染, 即便是你在前一个宏任务里面更改了 dom 元素的属性, 也不一定会在下一个宏任务触发前发生渲染, 而 requestAnimationFrame 一定是伴随着渲染而触发的, 没有渲染就不会触发 requestAnimationFrame
浏览器真实的执行顺序是 宏任务 -> 微任务队列清空 -> 判断是否执行渲染 -> 需要渲染 ? requestAnimationFrame 宏任务触发 + 微任务队列清空 + 渲染
通过下面的例子,能更直观的理解这个过程
右边的示例, 每次点击 div 元素的时候, 都会调用 requestAnimationFrame 和 setTimeout
多次点击 div 后, 你会发现大部分的输出的都是 2 1
加上一段代码, 每次点击都会更新 dom 的宽度, 你会发现输出大部分都是 1 2,只有少部分的 2 1
这是因为浏览器要发生频繁的渲染, 伴随着 requestAnimationFrame 的触发比 setTimeout 更加快, 但是浏览器仍然会跳过一些渲染
electron 的权限配置
管理员权限(windows)
如果有修改用户注册表的需求, 需要获取管理员权限, 可以在 electron-builder 里面设置
程序在启动的时候就会请求管理员权限
媒体权限
- Mac os
- Windows
如果要获取摄像头和麦克风的权限, 首先要创建一个 plist , plist 里面可以指定以下的权限 
plist 如下所示
plist 必须完整, 如果你删掉了一些内容, 那么应用会崩溃😭
然后配置 electron-builder
配置 plist
加上请求权限的提示
屏幕录制的提示
辅助功能
如果你的 electron 需要调用一些 .node 文件去做操作,比如锁屏,那就要在 plist 里面指定 disable-library-validation
在应用中请求并确认权限
查看是否有 摄像头 权限,第一次会弹出询问框
查看是否有 麦克风 权限,第一次会弹出询问框
查看是否有 屏幕录制 权限
查看是否有 辅助功能 权限
可以根据上面的权限状态弹出提示框来提示用户打开权限
Windows 在权限配置上比较简单, 直接在应用中查看是否有权限即可,默认是有的
查看是否有 摄像头 权限
查看是否有 麦克风 权限
查看是否有 屏幕录制 权限
查看是否有 辅助功能 权限
阻止系统的键盘事件
起因
最近在用 electron 开发一个线上的考试应用,有一个需求是在监考模式下拦截 用户的键盘事件,防止用户锁屏或者使用其它快捷键切换应用,在 github 翻了一圈,没有找到合适的方案 o(╥﹏╥)o,所以用 rust 写了一个 lib,然后用 napi-rs 打包成 nodejs 库。下面记录下整个流程。
mac os
开发 mac os 应用,需要使用 cocoa 库,mozilla 开发了一个 rust 版本的库core-foundation-rs
使用 core_graphics 的 CGEventTap 可以拦截键盘事件
_16use core_graphics::event::{_16 CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType::KeyDown,_16};_16_16fn handle_key_event() {_16 let cg_event_tap = CGEventTap::new(_16 CGEventTapLocation::Session,_16 // 插入队首_16 CGEventTapPlacement::HeadInsertEventTap,_16 // 默认行为过滤_16 CGEventTapOptions::Default,_16 // 监听的事件_16 vec![CGEventType::KeyDown],_16 callback,_16 );_16}
构造 CGEventTap 的 5 参数依次表示
- 监听事件的区域 直接选 CGEventTapLocation::Session 即可
- 插入队首还是队尾,当一个事件触发的时候,会依次调用回掉队列上面的回调函数,前面的回调函数可以阻止后面的回调函数触发,这里插入队首部,阻止后面的默认系统行为触发
- 只监听事件的触发还是可以阻止后面的行为触发 这里选择 Default
- 监听的事件,当前只监听键盘 KeyDown 事件
- 回调函数
callback
callback 接受 3 个参数,分别是 CGEventTapProxy, CGEventType, CGEvent
添加 callback 函数
callback 函数可以
判断 event 类型
event 类型需要通过判断当前的 CGEventType 是否
获取 keycode
通过 get_integer_value_field 拿到 keycode
判断是否是 meta 键
通过 event.get_flags() 判断 CGEventFlags
禁用某个键
设置 key event 为 NULL , 禁用这个键,比如禁用 control
上面的代码,还要加入事件循环中才能生效
_19use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop};_19_19let current: CFRunLoop = CFRunLoop::get_current();_19_19match cg_event_tap {_19 Ok(tap) => unsafe {_19 let loop_source = tap_19 .mach_port_19 .create_runloop_source(0)_19 .expect("sames broken");_19_19 current.add_source(&loop_source, kCFRunLoopCommonModes.clone());_19_19 tap.enable();_19_19 CFRunLoop::run_current();_19 },_19 Err(_) => panic!("can't prevent key event"),_19}
最终得到在 mac os 下的代码如下所示
在 mac os 需要在隐私和安全性里面打开对应程序的辅助功能,该程序才能生效
_51use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop};_51use core_graphics::event::{_51 CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,EventField,CGEventFlags,_51};_51_51fn handle_key_event() {_51 let cg_event_tap = CGEventTap::new(_51 CGEventTapLocation::Session,_51 // 插入队首_51 CGEventTapPlacement::HeadInsertEventTap,_51 // 默认行为过滤_51 CGEventTapOptions::Default,_51 // 监听的事件_51 vec![CGEventType::KeyDown],_51 |proxy: *const std::ffi::c_void, event_type: CGEventType, event: &CGEvent| match event_type {_51 CGEventType::KeyDown => {_51 let key = event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);_51_51 // 判断 control 键是否摁下_51 let is_control_press =_51 CGEventFlags::CGEventFlagControl & event.get_flags() == CGEventFlags::CGEventFlagControl;_51_51 if is_control_press {_51 // 禁用这个快捷键_51 event.set_type(CGEventType::Null);_51 }_51_51 Some(event.clone())_51 }_51 _ => Some(event.clone()),_51 },_51 );_51_51 let current: CFRunLoop = CFRunLoop::get_current();_51_51 match cg_event_tap {_51 Ok(tap) => unsafe {_51 let loop_source = tap_51 .mach_port_51 .create_runloop_source(0)_51 .expect("sames broken");_51_51 current.add_source(&loop_source, kCFRunLoopCommonModes.clone());_51_51 tap.enable();_51_51 CFRunLoop::run_current();_51 },_51 Err(_) => panic!("can't prevent key event"),_51 }_51}
windows
windows 下面实现锁屏的功能实在有点复杂,除了写键盘的事件 hook , 还要修改用户的注册表
这里使用了一个第三方库rdev来实现,rdev本质上还是调用了 windows api SetWindowsHookExA 来实现的
微软官方维护了一个 windows-rs 的 crate,也可以通过它来实现相应的功能
具体使用 rdev grab 的代码就不再详细说了
_10rdev::grab(move |ev| match ev.event_type {_10 EventType::KeyPress(key) => {_10 if should_restrict(key) {_10 None_10 } else {_10 Some(ev)_10 }_10 }_10 _ => Some(ev),_10});
rdev 可以拦截绝大部分 windows 的按键,但是有一些键很特殊,是不能通过写 hook 来拦截的,比如这三个键
win + l锁屏win + g弹出 windows gamectrl + alt + deleteCAD
上面三个键只能通过修改用户的注册表来实现拦截
修改注册表需要 windows 的管理员权限,必须要请求管理员权限
win + l 的注册表位于 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\System 将 DisableLockWorkstation 改为 1 即可禁用锁屏
win + g 位于 HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR 将 AppCaptureEnabled 设置成 0 即可禁止
cad 位于 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\taskmgr.exe 将 Debugger 设置成 Hotkey Disabled 即可禁止
如果是在 electron 里面直接使用 nodejs 的库 regedit 即可,使用 regedit 禁用 win + l 示例如下
_24// 禁用 win + l 锁屏_24import regeditOrigin from "regedit"_24_24const regedit = regeditOrigin.promisified_24_24async function main() {_24 const winLRegisterPath =_24 "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"_24_24 try {_24 await regedit.createKey([winLRegisterPath])_24 } catch (error) {_24 console.error(error)_24 }_24_24 await regedit.putValue({_24 [winLRegisterPath]: {_24 DisableLockWorkstation: {_24 type: "REG_DWORD",_24 value: 1,_24 },_24 },_24 })_24}
regedit 在调用的时候需要将 HKEY_CURRENT_USER 需要改成 HKCU
如法炮制即可禁用剩余的 win 快捷键,使用 napi-rs 打包, 用 github actions 编译成 .node 文件, 便可在 electron 中使用实现 霸屏 的功能
真不好搞 😭