文档
视口
Canvas 绘图工具和 Three.js 渲染共享核心几何体,同时保持编辑和播放状态明确。
从英文自动翻译。 查看英文原文
视口组件
apps/desktop/src/viewport/index.ts 出口 Editor2D, View3D,它们的道具类型,可用的工具标识符,以及 solidsFromProfiles。两个组件都填充其父组件:提供一个具有非零宽度和高度的容器。核心 CAD 实体和 CAM 程序是共享数据模型。
2D 编辑器渲染到 Canvas 2D 并维护自己的选择、相机和命令历史记录。 3D 查看器使用 Three.js、轨道和变换控件、异步网格作业和程序播放。渲染路径或实体与保存它或验证制造操作无关。
整合合同
Editor2DProps 需要 drawing, onChange, 和 selectedTool。将发出的图纸送回道具中。该组件识别它自己发出的文档对象;从外部替换文档会加载新绘图并重置编辑器状态。
import { createSignal } from "solid-js"
import { Editor2D, type ToolId } from "./viewport/index.ts"
const [drawing, setDrawing] = createSignal(initialDrawing)
const [tool, setTool] = createSignal<ToolId>("select")
<Editor2D drawing={drawing()} onChange={setDrawing} selectedTool={tool()} /> 选择会排除锁定的实体和锁定图层上的实体。不可见实体和隐藏层被排除在渲染的空间索引之外。新实体接收文档唯一标识符,并在可用时将其分配给可见的未锁定层。
绘图工具和手势
工具注册表具有用于选择、直线、矩形、圆、椭圆、折线、圆弧、文本、尺寸、移动、旋转、缩放、镜像、偏移、修剪和圆角的工厂。 AVAILABLE_TOOLS 来自那些注册工厂。 startTool 另外认识到 chamfer 通过圆角工具工厂,但倒角未在该数组中公布。消费者应使用实际的注册表来决定启用哪些工具。
- 滚轮或捏合缩放;按空格键拖动、按中键拖动和平移鼠标右键。
F适合索引绘图。删除或退格键可删除解锁的选择。Cmd/Ctrl+Z撤销;Cmd/Ctrl+Shift+Z或Cmd/Ctrl+Y重做。- Enter 提交多段线。 Escape 被传递到活动工具以取消其当前手势。
- 在文本处于活动状态时,在放置前键入字符或退格键;初始文本是
Text.
在工具调度之前,指针位置会转换为世界坐标。捕捉结合了空间索引、几何候选和依赖于相机的网格;选择命中容差对应于八个屏幕像素。工具机生成命令和预览,而不是编写文件或调用 API。
3D 渲染和播放
View3D 接受图纸、实体、可选型材挤压、 cam.Program、库存尺寸和工具定义。它支持透视、顶、底、前、后、左、右、等距和四视图预设。显示模式有阴影、带边缘阴影、线框和X射线。照明、材质属性、裁剪部分、网格、轴和原点都可以通过道具进行配置。
控制播放 playing, playbackTime 在几秒钟内, playbackRate, 和 onPlaybackTimeChange。回放可扩展钻孔循环、细分圆形/螺旋运动、保留停留时间并清晰地绘制快速移动。刀具图和可选的默认刀具供应刀具、刀柄和刀柄几何形状;假定的几何形状在视觉上是可区分的。
选择回调报告可靠的索引。变换控件通过以下方式发出位置、旋转和缩放 onTransformCommit;调用者决定如何保持这种转换。刀具路径叠加可以显示点和标签、点类型过滤器、圆弧中心、圆弧半径和毛坯透明度。
回放是一种视觉表现
观看者对所提供的程序进行动画处理并绘制库存和工具。它不执行材料去除。碰撞和机器包络检查属于CAM验证模块;查看程序并不构成机器认可。
坐标和单位
核心绘图坐标和毛坯尺寸使用毫米。绘制角度和Three.js旋转组件使用弧度;请勿将该约定应用于明确使用度数的 CAM 旋转轴值或特征输入。 viewport/units.ts 包含显示毫米和英寸的转换助手。在输入/输出边界处进行转换,以便捕捉、几何图形和刀具路径运动保持一致的单位系统。
2D 相机将世界坐标映射到 CSS 像素;背景画布尺寸包含设备像素比。调整大小通过更新相机和后备缓冲区 ResizeObserver。相机比例决定网格间距和屏幕空间命中容差。
英寸显示精确地转换为每英寸 25.4 毫米,长度默认为 3 小数位(apps/desktop/src/viewport/units.ts)。在 2D 编辑器中,每个滚轮步长都会缩放 1.15 倍,比例被限制在 0.001 和 100、000 之间,并且适合视图留下 10% 的边距 (editor2d/camera.ts).
资源和生命周期
2D 组件取消排队的动画帧并在卸载时断开其调整大小观察器的连接。 3D 组件发布调整大小和可见性侦听器、轨道和变换控件、网格工作器、动画调度、几何图形、材质、精灵、工具网格和 WebGL 渲染器。
帧调度程序会合并无效并在相机阻尼或回放需要时请求更多帧。能见度变化导致工作暂停。这是一个调度策略,而不是每个型号和设备的有保证的帧速率预算。
模块图
editor2d/editor2d.tsx:受控组件、指针调度、相机、选择、历史记录和画布绘画。editor2d/tools/:手势状态机和命令构建。editor2d/camera.ts,render.ts, 和snap-pipeline.ts:坐标变换、实体渲染、拾取和捕捉编排。spatial/grid.ts:绘图空间索引。view3d/view3d.tsx:场景合成、摄像机控制、选择、变换、库存和工具。view3d/playback.ts,toolpath-overlays.ts, 和modeling.ts:运动时间线、叠加数据和轮廓到实体的转换。view3d/mesh-jobs.ts和mesh-worker.ts:背景几何构造和工作所有权。
工具可用性
ToolId 描述更大的工具词汇; AVAILABLE_TOOLS 描述已注册的子集。对于交互式工具,请使用 startTool(id, lookup, context, text)。选择查找查询候选实体;上下文提供当前选择和实体访问; text 提供注释内容。
失踪的工厂返回 undefined。明确地处理该问题,而不是将禁用或未实现的工具呈现为活动编辑模式。修改现有几何图形的工具需要当前选择和实体上下文;调用没有这些依赖项的工厂不足以进行有用的编辑。
网格划分和验证
对于估计顶点数高于 50、000 且浏览器 Worker 支持的实体,查看器会在模块 Worker 中对网格构建进行排队。较小的实体同步网格化。网格队列处理完成、陈旧作业、工作错误和清理。阈值是用于调度的估计值,而不是记录的模型大小限制。
bun test apps/desktop/src/viewport
bun run --filter @cadcamfun/desktop typecheck 逻辑测试涵盖渲染助手、工具手势、空间索引、播放和叠加。浏览器/WebGL 行为还取决于图形环境;在更改相机控制、场景生命周期或渲染时检查实际视口交互。
受控播放示例
家长可以拥有传输控制,而无需复制观看者的运动时间线。该组件接受已构建的 CAM 程序并控制时间、播放状态、速度和视口预设。它的导入路径假设有一个文件直接位于 apps/desktop/src.
import { createSignal } from "solid-js"
import { cam } from "@cadcamfun/core"
import { View3D } from "./viewport/index.ts"
export function ProgramPreview(props: { program: cam.Program }) {
const [seconds, setSeconds] = createSignal(0)
const [playing, setPlaying] = createSignal(false)
return <div style={{ height: "600px" }}>
<button onClick={() => setPlaying(value => !value)}>Play / pause</button>
<button onClick={() => { setSeconds(0); setPlaying(true) }}>Restart</button>
<View3D
program={props.program}
preset="isometric"
playing={playing()}
playbackTime={seconds()}
playbackRate={1}
onPlaybackTimeChange={setSeconds}
stock={{ origin: { x: 0, y: 0, z: -10 }, size: { x: 100, y: 80, z: 10 } }}
/>
</div>
} 播放构建器从原点开始,将循环扩展为移动,并根据行进长度和进给计算持续时间。其默认快速速度为 5,000 mm/min,默认圆弧细分公差为 0.1 mm。这些是预览假设,而不是测量的机器加速度或控制器时序。停留命令保留其规定的秒数;评论和仅限定向的条目不会增加线性行程。
工具颜色由每个工具标识符第一次出现时分配,并在其路径中保留。为装配几何体提供工具图/默认工具;单独的彩色路径并不能确定验证所使用的刀具或支架尺寸。
编辑配方和当前工具限制
| 手术 | 手势和前提条件 | 目前的行为 |
|---|---|---|
| 抵消 | 选择一个支持的实体,然后拖动; Shift 键会选择光标下的实体。 | 直线/折线/矩形产生折线结果。圆和圆弧实现平移中心,同时保留半径;该工具不是一般的曲线同心偏移操作。 |
| 修剪/延伸 | 首先选择源实体和边界实体,激活“修剪”,然后单击目标。 Alt 选择扩展模式。 | 该实现搜索其他选定实体的交集。它改变直线和折线;不支持的目标类型或缺失的交集不会产生任何编辑。 |
| 圆角/倒角 | 选择两条相交线。 Shift 切换到倒角。 | 共享工具使用 5 mm 的初始半径,并用修剪线和连接折线替换该对。它不是任意的实边圆角。 |
| 变换实体 | 提供 onTransformCommit,选择一个网格,然后激活平移/旋转/缩放。 | 回调报告实体索引和转换值。在四视图模式下禁用拾取和变换小控件。 |
这些细节来自手势实现而不是调色板标签。在提交之前检查预览,如果结果与预期几何图形不同,请使用“撤消”。不返回命令的工具可能意味着其几何先决条件未得到满足,而不是编辑器丢失了指针事件。
渲染和网格划分失败
默认情况下,后台网格队列最多接受 128 个待处理作业,一次运行一个作业,并且每个作业有一个默认的 15 秒超时。重置场景会终止工作线程并清除排队的工作;过时的响应将被忽略。无效的工作响应、消息解码错误、超时和队列容量故障通过队列回调报告错误。
| 症状 | 行动 |
|---|---|
| 没有几何图形的空画布 | 为父级指定非零大小,检查实体/图层可见性并适合视图。 |
| 没有什么可以选择的 | 检查 2D 中的实体/层锁;检查 3D 中的四视图模式和变换状态。 |
| 网格超时或队列已满 | 降低场景复杂性或同时提交的实体数量并检查工作人员错误。失败的批次将被清除,而不是作为已完成的作业部分安装。 |
| 播放时间好像不对 | 检查程序进给、停留移动、快速假设以及更改的程序是否重置父时间。 |
| WebGL 不可用 | 使用支持的图形环境并检查组件的不可用渲染器状态;逻辑测试无法证明 GPU 可用性。 |
相关测试包括 editor2d/tools/offset.test.ts, annotation-transform.test.ts, spatial/grid.test.ts, view3d/playback.test.ts, 和 toolpath-overlays.test.ts。网格队列的实现是 view3d/mesh-jobs.ts.
详细显示单位
编辑器总是以毫米为单位进行工作。仅使用以下确切因素转换用户读取和键入的内容。存储的图形自己的单位保留 mm 或 inch;额外的单位仅供显示选择。
| 单元 | 标签 | 每单位毫米 | 默认小数位 | G代码程序单元 |
|---|---|---|---|---|
mm | 毫米 | 1 | 3 | 毫米 |
um | µm | 0.001 | 1 | 毫米 |
cm | 厘米 | 10 | 4 | 毫米 |
m | m | 1000 | 6 | 毫米 |
inch | 英寸 | 25.4 | 4 | 英寸 |
thou | 你 | 0.0254 | 2 | 英寸 |
import { formatLength, parseLength, formatAngle, programUnitsOf } from "./units"
formatLength(25.4, "inch") // "1.0000 inch"
formatLength(12.5) // "12.500 mm"
formatLength(0.0254, "thou") // "1.00 thou"
parseLength("2 in", "inch") // 50.8 (a trailing unit word is ignored)
parseLength("abc") // undefined
formatAngle(Math.PI / 2) // "90.0°"
programUnitsOf("thou") // "inch" parseLength 删除尾随字母后缀(包括 µ 和 °)并解释当前显示单位中的数字。后缀不用于选择单位,因此输入 2 mm 而显示单位为英寸则为 50.8 毫米。空、非数字和无限输入返回 undefined,在这种情况下编辑者应该保留以前的值。
2D 相机和网格
worldToScreen 翻转 Y,使世界 +Y 在屏幕上指向上方。| 常数/函数 | 价值 | 行为 |
|---|---|---|
ZOOM_STEP | 1.15 | 每轮步距系数; zoomAt 保持光标下的世界点固定。 |
MIN_SCALE / MAX_SCALE | 0.001 / 100,000 像素/毫米 | 比例被限制在这个范围内。 |
FIT_MARGIN | 0.1 | 视口的一部分保留为边距 cameraForBounds. |
createCamera() | 中心 (0, 0),比例 1,800 × 600 | 第一次调整大小之前的初始相机。 |
gridStep(scale, major = 10, minPixels = 8) | 1-2-5 梯子 | 使网格点间隔至少 8 像素的最小步长。 |
const camera = createCamera({ x: 0, y: 0 }, 2, 800, 600) // 2 px per mm
worldToScreen(camera, { x: 10, y: 10 }) // { x: 420, y: 280 }
screenToWorld(camera, { x: 420, y: 280 }) // { x: 10, y: 10 }
gridStep(2) // 5 -> 5 mm apart = 10 px
gridStep(0.5) // 20 -> 20 mm apart = 10 px 渲染剔除实体 visibleBounds(camera)。捕捉还避免了扫描整个绘图:统一的空间网格返回边界位于捕捉容差加上 2 mm 候选边距内的实体,并且只有这些实体才会传递到核心 findSnap。这使得指针在具有许多实体的绘图中快速移动。
捕捉参考
捕捉类型按优先顺序进行尝试。默认顺序与旧版编辑器匹配: endpoint, midpoint, center, intersection, perpendicular, tangent, nearest, 和 quadrant。端点是源实体的顶点,而不是其细分的顶点。
| 环境 | 意义 |
|---|---|
enabled | 主开关; false 禁用所有捕捉。 |
types | 启用的捕捉类型,优先级最高。 |
tolerance | 捕捉半径(以毫米为单位);它不依赖于变焦。 |
gridEnabled, gridSpacing | 当没有对象捕捉应用时捕捉到网格。 |
referencePoint | 从中绘制的点。 perpendicular 和 tangent 没有它什么也找不到。 |
管道返回快照、光标下实体的 ID(最接近的第一个,选择工具使用的实体)以及考虑的候选者数量。
刀具路径回放如何计时
buildPlayback(program, rapidRate = 5000, tolerance = 0.1) 将 CAM 程序转换为定时段。每个移动的持续时间是其长度除以进给量(或急流的速度),乘以 60,即秒数。居住者无需移动即可添加自己的时间。注释、方向移动和固定循环不会绘制为段。弧线被分成和弦,其矢矢留在其中 tolerance (在 2 和 65 之间,每弧 536 个弦),并且 Z 沿着螺旋弧进行插值。
const playback = buildPlayback(program) // rapids at 5000 mm/min
playback.duration // total seconds
positionAt(playback, 12.5) // tool position at t = 12.5 s
toolAt(playback, 12.5) // toolId cutting at that time
toolColors(program) // Map toolId -> colour, first use order
// A 10 mm feed move at 600 mm/min lasts 10 / 600 × 60 = 1 s.
// The same 10 mm as a rapid lasts 10 / 5000 × 60 = 0.12 s. segmentIndexAt 通过二分查找找到播放段,因此即使对于长程序,查找也很便宜。寻找越过终点返回最终位置,并且 toolAt 然后报告最后一个工具。播放时间是基于编程提要的视觉估计;它忽略了机器加速度,因此真实机器需要更长的时间。
视口实践
- 在边缘转换:以毫米和弧度存储和计算,并且格式仅用于显示
formatLength和formatAngle. - 切勿在之后重复使用已解析的长度
parseLength回报undefined;保留之前的值并显示验证提示。 - 添加绘图工具时,将其注册到工具工厂表中,以便工具栏、快捷方式和测试通过相同的 ID 对其进行寻址。
- 保持快照候选查询的空间性。将每个实体传递给
findSnap每次指针移动都会破坏索引。 - 将播放持续时间视为预览,而不是周期时间报价。
大规模命中测试:空间网格
2D 编辑器以 60、10 实体的 000 fps 为目标,因此命中测试和剔除不会扫描每个指针事件的每个实体。 viewport/spatial/grid.ts 保持统一的网格:每个项目都在其边界框接触的每个单元格中注册,并且查询按插入顺序返回边界与查询框相交的所有项目。结果是准确的,不是样品。
| 环境 | 价值 | 为什么 |
|---|---|---|
| 默认单元格大小 | 50 世界单位(毫米) | 适合宽约 1 米的绘图 |
| 最小细胞尺寸 | 1e-6 | 防止零或负大小,这将永远循环 |
| 每个项目的单元格限制 | 4,096 | 较大的项目(整个工作表上的一条长线)将被放在一边而不是每个单元格中进行跟踪 |
| 方法 | 行为 |
|---|---|
build(items) | 从头开始重建 |
upsert(id, item) | 按 id 插入或替换一项 |
remove(id) | 消除;未知的 id 会被忽略 |
query(bounds) | 边界与框相交的 ID |
访问虚线查询框触及的单元格; A 和长线被返回,因为它们的边界与其相交,而 B 则不然。
3D 网格划分队列
实体在 Web Worker 中进行网格划分,因此 UI 线程永远不会阻塞。 MeshWorkerQueue 中 view3d/mesh-jobs.ts 一次运行一项作业:
| 规则 | 价值 | 失败消息 |
|---|---|---|
| 容量(排队+运行) | 128 | 3D 网格队列已满。降低场景复杂度。 |
| 每个作业的超时时间 | 15 s | 3D 网格划分超时。降低场景复杂度。 |
| 工人错误 | — | 工作人员的消息,或“网格工作人员意外停止”。 |
| 无法解码的回复 | — | 网格工作人员无法解码其响应。 |
| 几何形状不好 | 职位必须是 Float32Array, 指数 Uint32Array | 网格工作器返回无效的几何体。 |
任何失败都会终止工作程序并清除整个队列,因此过时的场景工作在新的编辑后永远不会落地;下一次提交会启动一个新的工人。回复与职位 ID 相匹配,因此已终止工作人员的迟到回复将被忽略。
2D 工具实现
| 文件 | 工具 |
|---|---|
tools/two-point.ts | 直线、矩形、直径圆、椭圆 |
tools/chained.ts | 折线、三点圆弧 |
tools/fillet-chamfer.ts | 将两条线之间的角倒圆或倒角 |
tools/offset.ts | 将折线、矩形、圆形或圆弧偏移一定距离 |
tools/trim.ts | 修剪和延伸 |
tools/select.ts | 单击、框选和添加/删除选择 |
tools/annotation-transform.ts | 移动和变换注释 |
每个工具都是一个小型状态机 onPointerDown, onPointerMove 和 onPointerUp,由一家工厂创建 tools/index.ts,每个调色板条目一个。指针位置经过 snap-pipeline.ts 在工具看到它们之前。