@mica-js/editor 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,7 +84,11 @@ await editor.executeCommand('schematic.undo');
84
84
 
85
85
  可选 `voltageReferences: { VCC: 5 }` 为同名原理图网络配置固定板级电压,接地网络保持 0 V。电源标记只命名网络,电压由该选项显式提供。
86
86
 
87
- 运行会话负责唯一帧调度器,以 25 ms 固定步长推进仿真;暂停时继续刷新 widget 而不推进仿真时间。话题数据通过两个 guest 之间的直连 RPC 通道传递,editor 配置绑定,不在 UI 线程转发 payload。位置、旋转和层级只属于编辑器;改变执行输入时先建立候选会话,全部准备成功后替换旧会话。文档与历史在准备失败时保持原值。
87
+ Canvas Editor 自动为 Widget 音频创建并管理 `AudioContext`,无需宿主传入音频对象。`canvas.run` 恢复播放;`canvas.pause`、重置和切回编辑模式会暂停音频,单步保持静音。浏览器可能要求用户手势:建议从点击事件直接调用运行命令;自动运行被阻止时,在运行中的画布内点击或按键可再次解锁。浏览器不支持 Web Audio 时仍可使用编辑器,只是不播放声音;音频初始化或恢复错误通过 `onDidError` 报告。替换运行时或销毁编辑器会释放对应音频上下文。
88
+
89
+ 工厂选项 `stepMs` 同时设置每步仿真时长与目标帧周期,单位为毫秒,默认 `20`(50 FPS);例如 `createCanvasEditor({ ...options, stepMs: 10 })` 对应每步 10 ms、目标 100 FPS。值必须为有限正数,转换后至少为 1 ps,且不得超过运行时时间范围。单步命令也使用此步长,创建后不能动态修改。
90
+
91
+ 运行会话负责唯一帧调度器,默认以 20 ms 固定步长推进仿真,目标帧率为 50 FPS。调度按单调时钟的绝对时间目标对齐,扣除执行耗时;单步过慢时跳过错过的时隙,不并发、不积压补步,因此过载时仿真慢于实时。暂停时仍按该节奏刷新 widget 而不推进仿真时间。话题数据通过两个 guest 之间的直连 RPC 通道传递,editor 配置绑定,不在 UI 线程转发 payload。位置、旋转和层级只属于编辑器;改变执行输入时先建立候选会话,全部准备成功后替换旧会话。文档与历史在准备失败时保持原值。
88
92
 
89
93
  library 的代码只在 sandbox 中执行,资源由 editor 通过只读文件系统预先读取。关闭 editor 会取消时钟、移除视图,并释放内部运行实例、RPC、bitmap 和 sandbox;外部 provider 保留宿主所有权。
90
94
 
package/dist/index.d.ts CHANGED
@@ -128,6 +128,8 @@ export declare interface ICanvasEditorOptions {
128
128
  readonly fileSystem: IFileSystemProvider_2;
129
129
  readonly deviceLibrary: IDeviceLibrary_2;
130
130
  readonly widgetLibrary: IWidgetLibrary_2;
131
+ /** Simulation step and target frame period in milliseconds; defaults to 20. */
132
+ readonly stepMs?: number;
131
133
  readonly readOnly?: boolean;
132
134
  /** Fixed board voltages by exact schematic net name; ignored without a schematic. Ground stays zero. */
133
135
  readonly voltageReferences?: Readonly<Record<string, number>>;
package/dist/index.js CHANGED
@@ -5529,22 +5529,33 @@ var CanvasRuntimeFactory = class {
5529
5529
  let widgetWorker;
5530
5530
  let simulation;
5531
5531
  let widgets;
5532
+ let audioContext;
5532
5533
  try {
5533
5534
  deviceWorker = createSimulationRuntimeGuestSandbox({ container });
5534
5535
  simulation = await createSimulationRuntimeFromWorker(deviceWorker, { topicPort: channel.port1 });
5535
5536
  widgetWorker = createWidgetHostGuestSandbox({ container });
5536
- widgets = createWidgetRuntime(await createWidgetSandboxFromWorker(widgetWorker, {
5537
+ const sandbox = await createWidgetSandboxFromWorker(widgetWorker, {
5537
5538
  container,
5538
5539
  devicePort: channel.port2
5539
- }));
5540
+ });
5541
+ if (typeof globalThis.AudioContext === "function") audioContext = new AudioContext();
5542
+ widgets = createWidgetRuntime(sandbox, { ...audioContext === void 0 ? {} : { audio: { context: audioContext } } });
5543
+ widgets.audio.suspend();
5540
5544
  const ownedSimulation = simulation;
5541
5545
  const ownedWidgets = widgets;
5542
5546
  let disposal;
5543
5547
  return {
5544
5548
  simulation,
5545
5549
  widgets,
5550
+ async resumeAudio() {
5551
+ if (!disposal && audioContext?.state === "suspended") await audioContext.resume();
5552
+ },
5546
5553
  dispose() {
5547
- return disposal ??= Promise.allSettled([ownedWidgets.dispose(), ownedSimulation.dispose()]).then((results) => {
5554
+ return disposal ??= Promise.allSettled([
5555
+ ownedWidgets.dispose(),
5556
+ ownedSimulation.dispose(),
5557
+ audioContext?.close()
5558
+ ]).then((results) => {
5548
5559
  const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
5549
5560
  if (errors.length) throw new AggregateError(errors, "Canvas runtimes failed to dispose.");
5550
5561
  });
@@ -5553,7 +5564,11 @@ var CanvasRuntimeFactory = class {
5553
5564
  } catch (error) {
5554
5565
  channel.port1.close();
5555
5566
  channel.port2.close();
5556
- await Promise.allSettled([widgets?.dispose() ?? widgetWorker?.terminate("Canvas initialization failed"), simulation?.dispose() ?? deviceWorker?.terminate("Canvas initialization failed")]);
5567
+ await Promise.allSettled([
5568
+ widgets?.dispose() ?? widgetWorker?.terminate("Canvas initialization failed"),
5569
+ simulation?.dispose() ?? deviceWorker?.terminate("Canvas initialization failed"),
5570
+ audioContext?.close()
5571
+ ]);
5557
5572
  throw error;
5558
5573
  }
5559
5574
  }
@@ -5623,16 +5638,31 @@ var CanvasSession = class CanvasSession {
5623
5638
  #simTime = timePs(0);
5624
5639
  #running = false;
5625
5640
  #timer;
5641
+ #nextFrame;
5626
5642
  #disposed = false;
5627
5643
  #queue = Promise.resolve();
5628
5644
  #disposal;
5629
- #step = toTimePs(25, "ms");
5645
+ #frameMs;
5646
+ #step;
5647
+ #unlockAudio = () => {
5648
+ const active = this.#active;
5649
+ if (this.#disposed || !this.#running || !active) return;
5650
+ active.runtimes.resumeAudio().catch((error) => {
5651
+ if (!this.#disposed && this.#active === active) this.#errors.fire(error);
5652
+ });
5653
+ };
5630
5654
  constructor(configuration, files, devices, widgets, factory) {
5631
5655
  this.configuration = configuration;
5632
5656
  this.files = files;
5633
5657
  this.devices = devices;
5634
5658
  this.widgets = widgets;
5635
5659
  this.factory = factory;
5660
+ this.#frameMs = configuration.stepMs ?? 20;
5661
+ if (!Number.isFinite(this.#frameMs) || this.#frameMs <= 0) throw new Error("stepMs must be a positive finite number.");
5662
+ this.#step = toTimePs(this.#frameMs, "ms");
5663
+ if (this.#step <= 0) throw new Error("stepMs must represent at least one picosecond.");
5664
+ configuration.container.addEventListener("pointerdown", this.#unlockAudio, true);
5665
+ configuration.container.addEventListener("keydown", this.#unlockAudio, true);
5636
5666
  }
5637
5667
  get mode() {
5638
5668
  return this.#mode;
@@ -5701,12 +5731,15 @@ var CanvasSession = class CanvasSession {
5701
5731
  if (this.#mode !== "simulation") throw new Error("Switch to simulation mode before running.");
5702
5732
  if (this.#running) return;
5703
5733
  this.#running = true;
5734
+ this.#active?.runtimes.widgets.audio.resume();
5735
+ this.#unlockAudio();
5704
5736
  this.#changes.fire();
5705
5737
  this.#schedule();
5706
5738
  }
5707
5739
  pause() {
5708
5740
  if (!this.#running) return;
5709
5741
  this.#running = false;
5742
+ this.#active?.runtimes.widgets.audio.suspend();
5710
5743
  this.#changes.fire();
5711
5744
  }
5712
5745
  step() {
@@ -5722,9 +5755,12 @@ var CanvasSession = class CanvasSession {
5722
5755
  dispose() {
5723
5756
  if (this.#disposal) return this.#disposal;
5724
5757
  this.#disposed = true;
5758
+ this.configuration.container.removeEventListener("pointerdown", this.#unlockAudio, true);
5759
+ this.configuration.container.removeEventListener("keydown", this.#unlockAudio, true);
5725
5760
  this.pause();
5726
5761
  if (this.#timer !== void 0) clearTimeout(this.#timer);
5727
5762
  this.#timer = void 0;
5763
+ this.#nextFrame = void 0;
5728
5764
  this.#disposal = this.#queue.then(async () => {
5729
5765
  const active = this.#active;
5730
5766
  this.#active = void 0;
@@ -5814,13 +5850,18 @@ var CanvasSession = class CanvasSession {
5814
5850
  }
5815
5851
  #schedule() {
5816
5852
  if (this.#disposed || !this.#active || this.#timer !== void 0) return;
5853
+ const now = performance.now();
5854
+ this.#nextFrame ??= now + this.#frameMs;
5855
+ if (this.#nextFrame < now) this.#nextFrame += Math.ceil((now - this.#nextFrame) / this.#frameMs) * this.#frameMs;
5817
5856
  this.#timer = setTimeout(() => {
5818
5857
  this.#timer = void 0;
5858
+ this.#nextFrame += this.#frameMs;
5819
5859
  this.#enqueue(() => this.#advance(this.#running ? this.#step : timePs(0))).then(() => this.#schedule(), (error) => {
5860
+ this.#nextFrame = void 0;
5820
5861
  this.pause();
5821
5862
  if (!this.#disposed) this.#errors.fire(error);
5822
5863
  });
5823
- }, 25);
5864
+ }, this.#nextFrame - now);
5824
5865
  }
5825
5866
  async #release(active) {
5826
5867
  for (const subscription of active.subscriptions) subscription.dispose();
@@ -7094,6 +7135,7 @@ async function createCanvasEditor(options) {
7094
7135
  uri,
7095
7136
  ...options.projectRoot === void 0 ? {} : { projectRoot: root },
7096
7137
  container: options.container,
7138
+ ...options.stepMs === void 0 ? {} : { stepMs: options.stepMs },
7097
7139
  ...options.voltageReferences === void 0 ? {} : { voltageReferences: Object.freeze(structuredClone(options.voltageReferences)) }
7098
7140
  });
7099
7141
  services.set(ICommandService, new SyncDescriptor(Commands));