机械/电气工程CAD设计组件
这是一个交互式的二维CAD设计工具,专为机械和电气工程图纸绘制设计。该组件支持自适应布局,提供了丰富的绘图工具和机床结构元素库。
功能特点
- 多种绘图工具:直线、矩形、圆形、弧线、多边形、文本等基本图形绘制
- 尺寸标注:支持长度测量和尺寸标注功能
- 机床组件库:内置常用的机床结构组件,如床身、立柱、主轴箱等
- 属性编辑:可以实时修改所有图形和组件的属性
- 缩放和平移:支持画布的缩放和平移,方便查看大型图纸
- 网格辅助:可调整网格大小,辅助精确绘图
- 可导出设计:将设计导出为PNG图片格式
- 自适应布局:适应不同屏幕尺寸和设备
使用方法
基本操作
- 选择工具:点击顶部工具栏中的绘图工具(如直线、矩形等)
- 绘制图形:在画布上点击并拖动鼠标来绘制图形
- 选择对象:点击"选择"工具,然后点击画布上的对象进行选择
- 移动对象:选中对象后,拖动它可以改变位置
- 编辑属性:选中对象后,右侧属性面板会显示该对象的属性,可以直接修改
- 删除对象:选中对象后,点击属性面板中的"删除对象"按钮
使用机床组件
- 浏览组件库:在右侧面板的"机床组件库"中查看可用组件
- 添加组件:将组件从库中拖放到画布上
- 调整大小和位置:通过属性面板或直接拖动来调整组件
视图控制
- 平移画布:在选择工具模式下,拖动空白区域可以平移画布
- 缩放画布:使用鼠标滚轮或点击右上角的缩放按钮
- 重置视图:点击右上角的"重置视图"按钮返回默认视图
导出设计
点击顶部导航栏中的"导出设计"按钮,将当前绘图保存为PNG图片。
自定义配置
样式设置
CAD设计器的颜色主题和样式可以通过修改CSS变量来自定义:
:root {
--background: #F5F5F7; /* 背景色 */
--card-bg: #FFFFFF; /* 面板背景色 */
--primary-text: #1D1D1F; /* 主文本颜色 */
--secondary-text: #86868B; /* 次要文本颜色 */
--accent-blue: #0066CC; /* 强调色(蓝) */
--border-color: #D2D2D7; /* 边框颜色 */
--grid-color: #E8E8ED; /* 网格线颜色 */
}
扩展机床组件库
可以通过编辑JavaScript代码中的ShapeFactory
对象来添加新的机床组件类型:
'new-component': (x, y, options = {}) => ({
type: 'machine-component',
componentType: 'new-component',
x,
y,
width: 100, // 组件默认宽度
height: 50, // 组件默认高度
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
})
然后在HTML中的组件库中添加对应的条目:
<div class="cd-component-item" draggable="true" data-type="new-component">新组件名称</div>
技术细节
该CAD设计工具基于HTML5 Canvas实现,使用了以下技术:
- HTML5 Canvas API用于绘图功能
- 纯JavaScript实现,无依赖
- CSS Grid和Flexbox用于响应式布局
- CSS变量用于主题定制
- 拖放API用于组件库交互
未来计划
- 添加保存和加载设计功能
- 支持图层管理
- 增加更多机床组件类型
- 实现尺寸自动约束功能
- 添加协作编辑功能
项目结构
效果展示
源码
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>机械/电气工程CAD设计工具</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="cad-designer">
<!-- 顶部导航栏 -->
<div class="cd-header">
<div class="cd-logo">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12h20M2 12a10 10 0 0 1 20 0M2 12a10 10 0 0 0 20 0M14 6l-4 12"></path>
</svg>
<span>机床CAD设计器</span>
</div>
<div class="cd-actions">
<button class="cd-btn cd-btn-primary" id="exportBtn">导出设计</button>
<button class="cd-btn" id="clearBtn">清空画布</button>
</div>
</div>
<!-- 主内容区域 -->
<div class="cd-content">
<!-- 工具栏 -->
<div class="cd-toolbar">
<div class="cd-toolbar-group">
<button class="cd-tool-btn active" data-tool="select">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3l7 7m0 0v-6m0 6h-6"></path>
</svg>
选择
</button>
<button class="cd-tool-btn" data-tool="line">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
</svg>
直线
</button>
<button class="cd-tool-btn" data-tool="rectangle">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
</svg>
矩形
</button>
<button class="cd-tool-btn" data-tool="circle">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
</svg>
圆形
</button>
<button class="cd-tool-btn" data-tool="text">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 7V4h16v3"></path>
<path d="M9 20h6"></path>
<path d="M12 4v16"></path>
</svg>
文本
</button>
</div>
<div class="cd-toolbar-group">
<button class="cd-tool-btn" data-tool="dimension">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"></path>
<path d="M3 12h18"></path>
<path d="M3 18h18"></path>
</svg>
标注
</button>
<button class="cd-tool-btn" data-tool="arc">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0"></path>
</svg>
弧线
</button>
<button class="cd-tool-btn" data-tool="polygon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
<path d="M2 17l10 5 10-5"></path>
<path d="M2 12l10 5 10-5"></path>
</svg>
多边形
</button>
</div>
<div class="cd-toolbar-group">
<select class="cd-select" id="lineWidth">
<option value="1">线宽: 1px</option>
<option value="2" selected>线宽: 2px</option>
<option value="3">线宽: 3px</option>
<option value="4">线宽: 4px</option>
</select>
<div class="cd-color-picker">
<span>颜色:</span>
<input type="color" id="strokeColor" value="#1D1D1F">
</div>
</div>
</div>
<!-- 主面板区域 -->
<div class="cd-main-panels">
<!-- 绘图区域 -->
<div class="cd-panel cd-drawing-panel">
<div class="cd-panel-header">
<h2>机床结构设计</h2>
<div class="cd-panel-actions">
<button class="cd-tool-btn" id="zoomIn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="M21 21l-4.3-4.3"></path>
<path d="M8 11h6"></path>
<path d="M11 8v6"></path>
</svg>
</button>
<button class="cd-tool-btn" id="zoomOut">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="M21 21l-4.3-4.3"></path>
<path d="M8 11h6"></path>
</svg>
</button>
<button class="cd-tool-btn" id="resetView">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
</button>
</div>
</div>
<div class="cd-panel-body">
<div class="cd-canvas-container">
<canvas id="cadCanvas"></canvas>
</div>
</div>
</div>
<!-- 右侧面板 -->
<div class="cd-side-panels">
<!-- 属性面板 -->
<div class="cd-panel cd-properties-panel">
<div class="cd-panel-header">
<h2>属性</h2>
</div>
<div class="cd-panel-body">
<div class="cd-properties-list" id="propertiesPanel">
<div class="cd-empty-state">
请选择一个对象以查看其属性
</div>
</div>
</div>
</div>
<!-- 机床组件库 -->
<div class="cd-panel cd-components-panel">
<div class="cd-panel-header">
<h2>机床组件库</h2>
<div class="cd-panel-actions">
<button class="cd-tool-btn" id="collapseAll">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 14 10 14 10 20"></polyline>
<polyline points="20 10 14 10 14 4"></polyline>
<line x1="14" y1="10" x2="21" y2="3"></line>
<line x1="3" y1="21" x2="10" y2="14"></line>
</svg>
</button>
</div>
</div>
<div class="cd-panel-body">
<div class="cd-search-bar">
<input type="text" class="cd-search-input" placeholder="搜索组件..." id="componentSearch">
</div>
<div class="cd-components-tree">
<div class="cd-component-category">
<div class="cd-category-header">
<span class="cd-expand-icon">▼</span>
机床主体
</div>
<div class="cd-category-items">
<div class="cd-component-item" draggable="true" data-type="machine-bed">床身</div>
<div class="cd-component-item" draggable="true" data-type="machine-column">立柱</div>
<div class="cd-component-item" draggable="true" data-type="machine-spindle">主轴箱</div>
</div>
</div>
<div class="cd-component-category">
<div class="cd-category-header">
<span class="cd-expand-icon">▼</span>
运动部件
</div>
<div class="cd-category-items">
<div class="cd-component-item" draggable="true" data-type="linear-guide">直线导轨</div>
<div class="cd-component-item" draggable="true" data-type="ball-screw">滚珠丝杠</div>
<div class="cd-component-item" draggable="true" data-type="servo-motor">伺服电机</div>
</div>
</div>
<div class="cd-component-category">
<div class="cd-category-header">
<span class="cd-expand-icon">▼</span>
控制部件
</div>
<div class="cd-category-items">
<div class="cd-component-item" draggable="true" data-type="control-panel">控制面板</div>
<div class="cd-component-item" draggable="true" data-type="electrical-cabinet">电气柜</div>
<div class="cd-component-item" draggable="true" data-type="limit-switch">限位开关</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部状态栏 -->
<div class="cd-status-bar">
<div class="cd-coordinates">
<span id="mouseCoordinates">X: 0, Y: 0</span>
</div>
<div class="cd-view-info">
<span id="zoomLevel">缩放: 100%</span>
</div>
<div class="cd-grid-settings">
<label>
<input type="checkbox" id="showGrid" checked>
显示网格
</label>
<select class="cd-select cd-select-sm" id="gridSize">
<option value="10">网格: 10px</option>
<option value="20" selected>网格: 20px</option>
<option value="50">网格: 50px</option>
</select>
</div>
</div>
</div>
</div>
<!-- 文本编辑模态框 -->
<div class="cd-modal" id="textModal">
<div class="cd-modal-content">
<div class="cd-modal-header">
<h3>添加文本</h3>
<button class="cd-modal-close" id="closeTextModal">×</button>
</div>
<div class="cd-modal-body">
<div class="cd-form-group">
<label for="textContent">文本内容</label>
<input type="text" class="cd-input" id="textContent" placeholder="输入文本内容...">
</div>
<div class="cd-form-group">
<label for="fontSize">字体大小</label>
<select class="cd-select" id="fontSize">
<option value="12">12px</option>
<option value="14" selected>14px</option>
<option value="16">16px</option>
<option value="18">18px</option>
<option value="20">20px</option>
</select>
</div>
</div>
<div class="cd-modal-footer">
<button class="cd-btn" id="cancelTextBtn">取消</button>
<button class="cd-btn cd-btn-primary" id="confirmTextBtn">确认</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
styles.css
/* 机械/电气工程CAD设计工具 - 苹果科技风格 */
:root {
/* 颜色变量 - 苹果风格 */
--background: #F5F5F7;
--card-bg: #FFFFFF;
--primary-text: #1D1D1F;
--secondary-text: #86868B;
--accent-blue: #0066CC;
--accent-green: #34C759;
--accent-orange: #FF9500;
--accent-red: #FF3B30;
--accent-purple: #5E5CE6;
--border-color: #D2D2D7;
--grid-color: #E8E8ED;
/* 阴影 */
--card-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
--header-shadow: 0 1px 5px rgba(0, 0, 0, 0.05);
/* 尺寸变量 */
--header-height: 60px;
--toolbar-height: 48px;
--status-bar-height: 36px;
--panels-gap: 16px;
--border-radius: 10px;
--input-height: 32px;
}
/* 基础样式 */
#cad-designer {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
color: var(--primary-text);
background-color: var(--background);
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
position: relative;
box-sizing: border-box;
margin: 0;
padding: 0;
overflow: hidden;
}
#cad-designer * {
box-sizing: border-box;
}
/* 顶部导航栏 */
.cd-header {
height: var(--header-height);
background-color: var(--card-bg);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
box-shadow: var(--header-shadow);
z-index: 10;
}
.cd-logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 500;
}
.cd-logo svg {
color: var(--accent-blue);
}
.cd-actions {
display: flex;
gap: 12px;
}
/* 主内容区域 */
.cd-content {
display: flex;
flex-direction: column;
flex: 1;
height: calc(100% - var(--header-height));
overflow: hidden;
position: relative;
}
/* 工具栏 */
.cd-toolbar {
height: var(--toolbar-height);
background-color: var(--card-bg);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
padding: 0 16px;
gap: 20px;
overflow-x: auto;
}
.cd-toolbar-group {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.cd-toolbar-group:not(:last-child) {
padding-right: 20px;
border-right: 1px solid var(--border-color);
}
.cd-tool-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 6px;
border: none;
background-color: transparent;
color: var(--primary-text);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.cd-tool-btn:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.cd-tool-btn.active {
background-color: rgba(0, 102, 204, 0.1);
color: var(--accent-blue);
}
.cd-tool-btn svg {
color: var(--secondary-text);
}
.cd-tool-btn.active svg {
color: var(--accent-blue);
}
/* 选择器样式 */
.cd-select {
height: var(--input-height);
padding: 0 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--primary-text);
font-size: 13px;
outline: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2386868B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
.cd-select:hover {
border-color: var(--secondary-text);
}
.cd-select:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}
.cd-select-sm {
height: 28px;
font-size: 12px;
padding: 0 8px;
padding-right: 28px;
}
/* 颜色选择器样式 */
.cd-color-picker {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.cd-color-picker input[type="color"] {
width: 28px;
height: 28px;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0;
cursor: pointer;
}
/* 主面板区域 - 使用Grid布局 */
.cd-main-panels {
display: grid;
grid-template-columns: 1fr 320px;
grid-gap: var(--panels-gap);
padding: var(--panels-gap);
flex: 1;
overflow: hidden;
height: calc(100% - var(--toolbar-height) - var(--status-bar-height));
}
/* 绘图面板样式 */
.cd-drawing-panel {
height: 100%;
}
/* 面板样式 */
.cd-panel {
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
display: flex;
flex-direction: column;
overflow: hidden;
}
.cd-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
}
.cd-panel-header h2 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.cd-panel-actions {
display: flex;
gap: 8px;
}
.cd-panel-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 右侧面板布局 */
.cd-side-panels {
display: grid;
grid-template-rows: 1fr 1fr;
grid-gap: var(--panels-gap);
overflow: hidden;
}
/* 画布容器 */
.cd-canvas-container {
width: 100%;
height: 100%;
overflow: auto;
position: relative;
background-color: var(--card-bg);
}
#cadCanvas {
position: absolute;
top: 0;
left: 0;
background-color: var(--card-bg);
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 20px 20px;
cursor: crosshair;
}
/* 属性面板 */
.cd-properties-panel {
display: flex;
flex-direction: column;
}
.cd-properties-list {
padding: 12px;
overflow-y: auto;
}
.cd-property-item {
margin-bottom: 12px;
}
.cd-property-item label {
display: block;
font-size: 12px;
color: var(--secondary-text);
margin-bottom: 4px;
}
.cd-property-value {
width: 100%;
height: var(--input-height);
padding: 0 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
font-size: 13px;
outline: none;
}
.cd-property-value:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}
/* 空状态样式 */
.cd-empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--secondary-text);
font-size: 14px;
text-align: center;
padding: 20px;
}
/* 组件库面板 */
.cd-components-panel {
display: flex;
flex-direction: column;
}
.cd-search-bar {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
}
.cd-search-input {
width: 100%;
height: var(--input-height);
padding: 0 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background-color: var(--background);
font-size: 13px;
outline: none;
}
.cd-search-input:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}
.cd-components-tree {
overflow-y: auto;
padding: 8px;
flex: 1;
}
.cd-component-category {
margin-bottom: 8px;
}
.cd-category-header {
padding: 8px 10px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border-radius: 6px;
transition: background-color 0.2s;
}
.cd-category-header:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.cd-expand-icon {
display: inline-block;
margin-right: 6px;
font-size: 10px;
transition: transform 0.2s;
}
.cd-category-header.collapsed .cd-expand-icon {
transform: rotate(-90deg);
}
.cd-category-items {
padding: 4px 0 4px 20px;
}
.cd-component-item {
padding: 6px 10px;
font-size: 13px;
cursor: grab;
border-radius: 4px;
transition: background-color 0.2s;
margin-bottom: 4px;
}
.cd-component-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.cd-component-item:active {
cursor: grabbing;
}
/* 底部状态栏 */
.cd-status-bar {
height: var(--status-bar-height);
background-color: var(--card-bg);
border-top: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
font-size: 12px;
color: var(--secondary-text);
}
.cd-grid-settings {
display: flex;
align-items: center;
gap: 12px;
}
/* 按钮样式 */
.cd-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--primary-text);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.cd-btn:hover {
background-color: var(--background);
}
.cd-btn-primary {
background-color: var(--accent-blue);
border-color: var(--accent-blue);
color: white;
}
.cd-btn-primary:hover {
background-color: #0055B3;
border-color: #0055B3;
}
/* 模态框样式 */
.cd-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.cd-modal.active {
display: flex;
}
.cd-modal-content {
background-color: var(--card-bg);
border-radius: var(--border-radius);
width: 90%;
max-width: 400px;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.cd-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}
.cd-modal-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.cd-modal-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: var(--secondary-text);
}
.cd-modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.cd-modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 15px 20px;
border-top: 1px solid var(--border-color);
}
/* 表单样式 */
.cd-form-group {
margin-bottom: 16px;
}
.cd-form-group label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
}
.cd-input {
width: 100%;
height: var(--input-height);
padding: 0 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
font-size: 13px;
outline: none;
}
.cd-input:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}
/* 响应式布局 */
@media (max-width: 992px) {
.cd-main-panels {
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
}
.cd-side-panels {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
}
}
@media (max-width: 768px) {
.cd-side-panels {
grid-template-columns: 1fr;
grid-template-rows: auto auto;
}
.cd-toolbar {
overflow-x: auto;
justify-content: flex-start;
}
.cd-toolbar-group {
flex-shrink: 0;
}
}
script.js
/**
* 机械/电气工程CAD设计工具 - JavaScript
*/
// 全局变量
let canvas, ctx;
let canvasWidth = 2000;
let canvasHeight = 1500;
let zoomLevel = 1;
let panOffset = { x: 0, y: 0 };
let isDrawing = false;
let isDragging = false;
let lastX = 0;
let lastY = 0;
let shapes = [];
let selectedShape = null;
let currentTool = 'select';
let tempShape = null;
let gridSize = 20;
let showGrid = true;
// 基本配置和设置
const config = {
strokeColor: '#1D1D1F',
lineWidth: 2,
fontSize: 14,
};
// 形状工厂对象
const ShapeFactory = {
line: (startX, startY, endX, endY, options = {}) => ({
type: 'line',
startX,
startY,
endX,
endY,
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
}),
rectangle: (x, y, width, height, options = {}) => ({
type: 'rectangle',
x,
y,
width,
height,
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
}),
circle: (centerX, centerY, radius, options = {}) => ({
type: 'circle',
centerX,
centerY,
radius,
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
}),
text: (x, y, content, options = {}) => ({
type: 'text',
x,
y,
content: content || '文本',
fontSize: options.fontSize || config.fontSize,
strokeColor: options.strokeColor || config.strokeColor,
id: Date.now().toString(),
}),
arc: (centerX, centerY, radius, startAngle, endAngle, options = {}) => ({
type: 'arc',
centerX,
centerY,
radius,
startAngle,
endAngle,
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
}),
polygon: (points, options = {}) => ({
type: 'polygon',
points: [...points], // 点的数组
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
}),
dimension: (startX, startY, endX, endY, options = {}) => ({
type: 'dimension',
startX,
startY,
endX,
endY,
value: Math.round(Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)) * 100) / 100,
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
}),
// 预定义机床组件
'machine-bed': (x, y, options = {}) => ({
type: 'machine-component',
componentType: 'machine-bed',
x,
y,
width: 200,
height: 80,
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
}),
'machine-column': (x, y, options = {}) => ({
type: 'machine-component',
componentType: 'machine-column',
x,
y,
width: 50,
height: 120,
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
}),
'machine-spindle': (x, y, options = {}) => ({
type: 'machine-component',
componentType: 'machine-spindle',
x,
y,
width: 80,
height: 60,
strokeColor: options.strokeColor || config.strokeColor,
lineWidth: options.lineWidth || config.lineWidth,
id: Date.now().toString(),
}),
};
// 初始化函数
function init() {
// 获取Canvas元素和绘图上下文
canvas = document.getElementById('cadCanvas');
ctx = canvas.getContext('2d');
// 设置画布尺寸
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// 绑定事件监听器
setupEventListeners();
// 渲染初始画布
render();
}
// 设置事件监听器
function setupEventListeners() {
// 鼠标事件
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('wheel', handleMouseWheel);
// 工具按钮点击事件
document.querySelectorAll('.cd-tool-btn[data-tool]').forEach(button => {
button.addEventListener('click', function() {
setActiveTool(this.getAttribute('data-tool'));
});
});
// 缩放和视图控制
document.getElementById('zoomIn').addEventListener('click', () => {
changeZoom(0.1);
});
document.getElementById('zoomOut').addEventListener('click', () => {
changeZoom(-0.1);
});
document.getElementById('resetView').addEventListener('click', () => {
zoomLevel = 1;
panOffset = { x: 0, y: 0 };
render();
updateZoomDisplay();
});
// 线宽和颜色选择器
document.getElementById('lineWidth').addEventListener('change', function() {
config.lineWidth = parseInt(this.value);
updateSelectedShapeProperties();
});
document.getElementById('strokeColor').addEventListener('input', function() {
config.strokeColor = this.value;
updateSelectedShapeProperties();
});
// 网格设置
document.getElementById('showGrid').addEventListener('change', function() {
showGrid = this.checked;
render();
});
document.getElementById('gridSize').addEventListener('change', function() {
gridSize = parseInt(this.value);
render();
});
// 导出和清空按钮
document.getElementById('exportBtn').addEventListener('click', exportDesign);
document.getElementById('clearBtn').addEventListener('click', clearCanvas);
// 文本模态框相关事件
document.getElementById('confirmTextBtn').addEventListener('click', confirmTextInput);
document.getElementById('cancelTextBtn').addEventListener('click', closeTextModal);
document.getElementById('closeTextModal').addEventListener('click', closeTextModal);
// 组件树折叠/展开事件
document.querySelectorAll('.cd-category-header').forEach(header => {
header.addEventListener('click', function() {
this.classList.toggle('collapsed');
const items = this.nextElementSibling;
if (this.classList.contains('collapsed')) {
items.style.display = 'none';
} else {
items.style.display = 'block';
}
});
});
// 组件拖放
document.querySelectorAll('.cd-component-item').forEach(item => {
item.addEventListener('dragstart', handleDragStart);
});
canvas.addEventListener('dragover', handleDragOver);
canvas.addEventListener('drop', handleDrop);
// 折叠所有按钮
document.getElementById('collapseAll').addEventListener('click', collapseAllCategories);
// 组件搜索
document.getElementById('componentSearch').addEventListener('input', filterComponents);
}
// 鼠标事件处理函数
function handleMouseDown(e) {
const pos = getCanvasCoordinates(e);
lastX = pos.x;
lastY = pos.y;
// 先检查是否有选中对象
if (currentTool === 'select') {
const clickedShape = findShapeAtPosition(pos.x, pos.y);
if (clickedShape) {
selectedShape = clickedShape;
isDragging = true;
updatePropertiesPanel();
render();
return;
} else {
selectedShape = null;
updatePropertiesPanel();
render();
}
// 如果没选中对象,则开始平移画布
isDragging = true;
canvas.style.cursor = 'grabbing';
return;
}
// 开始绘制
isDrawing = true;
const options = {
strokeColor: config.strokeColor,
lineWidth: config.lineWidth,
fontSize: config.fontSize,
};
// 根据当前工具创建临时形状
switch (currentTool) {
case 'line':
tempShape = ShapeFactory.line(pos.x, pos.y, pos.x, pos.y, options);
break;
case 'rectangle':
tempShape = ShapeFactory.rectangle(pos.x, pos.y, 0, 0, options);
break;
case 'circle':
tempShape = ShapeFactory.circle(pos.x, pos.y, 0, options);
break;
case 'text':
openTextModal(pos.x, pos.y);
isDrawing = false;
break;
case 'arc':
tempShape = ShapeFactory.arc(pos.x, pos.y, 0, 0, Math.PI, options);
break;
case 'polygon':
if (!tempShape) {
tempShape = ShapeFactory.polygon([[pos.x, pos.y]], options);
} else {
tempShape.points.push([pos.x, pos.y]);
if (tempShape.points.length > 2) {
// 检查是否点击了第一个点以闭合多边形
const firstPoint = tempShape.points[0];
const dx = firstPoint[0] - pos.x;
const dy = firstPoint[1] - pos.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 10) {
shapes.push(tempShape);
tempShape = null;
isDrawing = false;
render();
}
}
}
break;
case 'dimension':
tempShape = ShapeFactory.dimension(pos.x, pos.y, pos.x, pos.y, options);
break;
}
}
function handleMouseMove(e) {
const pos = getCanvasCoordinates(e);
// 更新坐标显示
document.getElementById('mouseCoordinates').textContent = `X: ${Math.round(pos.x)}, Y: ${Math.round(pos.y)}`;
if (isDragging && currentTool === 'select') {
if (selectedShape) {
// 移动选中的形状
moveShape(selectedShape, pos.x - lastX, pos.y - lastY);
lastX = pos.x;
lastY = pos.y;
render();
} else {
// 平移画布
panOffset.x += pos.x - lastX;
panOffset.y += pos.y - lastY;
lastX = pos.x;
lastY = pos.y;
render();
}
return;
}
if (!isDrawing) return;
// 更新临时形状
if (tempShape) {
switch (tempShape.type) {
case 'line':
tempShape.endX = pos.x;
tempShape.endY = pos.y;
break;
case 'rectangle':
tempShape.width = pos.x - tempShape.x;
tempShape.height = pos.y - tempShape.y;
break;
case 'circle':
const dx = pos.x - tempShape.centerX;
const dy = pos.y - tempShape.centerY;
tempShape.radius = Math.sqrt(dx * dx + dy * dy);
break;
case 'arc':
const dxArc = pos.x - tempShape.centerX;
const dyArc = pos.y - tempShape.centerY;
tempShape.radius = Math.sqrt(dxArc * dxArc + dyArc * dyArc);
tempShape.endAngle = Math.atan2(dyArc, dxArc);
break;
case 'polygon':
if (tempShape.points.length > 0) {
// 更新最后一个点的位置(预览)
const lastIdx = tempShape.points.length - 1;
tempShape.previewX = pos.x;
tempShape.previewY = pos.y;
}
break;
case 'dimension':
tempShape.endX = pos.x;
tempShape.endY = pos.y;
tempShape.value = Math.round(Math.sqrt(Math.pow(tempShape.endX - tempShape.startX, 2) + Math.pow(tempShape.endY - tempShape.startY, 2)) * 100) / 100;
break;
}
render();
}
}
function handleMouseUp(e) {
if (!isDrawing && !isDragging) return;
if (isDrawing && tempShape) {
// 完成绘制,将临时形状添加到形状数组
if (tempShape.type !== 'polygon') {
shapes.push(tempShape);
tempShape = null;
} else if (tempShape.type === 'polygon' && tempShape.points.length === 1) {
// 多边形需要至少2个点,所以在第一个点不添加
tempShape.points.push([tempShape.previewX, tempShape.previewY]);
}
}
isDrawing = false;
isDragging = false;
canvas.style.cursor = 'crosshair';
render();
}
function handleMouseWheel(e) {
e.preventDefault();
const delta = e.deltaY < 0 ? 0.1 : -0.1;
changeZoom(delta, e);
}
// 缩放控制
function changeZoom(delta, e) {
const oldZoom = zoomLevel;
zoomLevel += delta;
zoomLevel = Math.max(0.1, Math.min(zoomLevel, 5)); // 限制缩放范围
if (e) {
// 计算鼠标位置的缩放
const pos = getCanvasCoordinates(e);
const factor = zoomLevel / oldZoom;
panOffset.x = pos.x - (pos.x - panOffset.x) * factor;
panOffset.y = pos.y - (pos.y - panOffset.y) * factor;
}
updateZoomDisplay();
render();
}
// 更新缩放显示
function updateZoomDisplay() {
document.getElementById('zoomLevel').textContent = `缩放: ${Math.round(zoomLevel * 100)}%`;
}
// 渲染函数
function render() {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 设置变换(缩放和平移)
ctx.save();
ctx.translate(panOffset.x, panOffset.y);
ctx.scale(zoomLevel, zoomLevel);
// 绘制网格
if (showGrid) {
drawGrid();
}
// 绘制所有形状
shapes.forEach(shape => {
drawShape(shape, shape === selectedShape);
});
// 绘制临时形状
if (tempShape) {
drawShape(tempShape, false);
// 对于多边形,绘制预览线
if (tempShape.type === 'polygon' && tempShape.points.length > 0 && tempShape.previewX !== undefined) {
const lastPoint = tempShape.points[tempShape.points.length - 1];
ctx.beginPath();
ctx.strokeStyle = tempShape.strokeColor;
ctx.lineWidth = tempShape.lineWidth;
ctx.moveTo(lastPoint[0], lastPoint[1]);
ctx.lineTo(tempShape.previewX, tempShape.previewY);
// 如果有多于2个点,绘制到第一个点的线
if (tempShape.points.length > 1) {
ctx.moveTo(tempShape.previewX, tempShape.previewY);
ctx.lineTo(tempShape.points[0][0], tempShape.points[0][1]);
}
ctx.stroke();
}
}
ctx.restore();
}
// 绘制网格
function drawGrid() {
const startX = Math.floor(-panOffset.x / zoomLevel / gridSize) * gridSize;
const startY = Math.floor(-panOffset.y / zoomLevel / gridSize) * gridSize;
const endX = Math.ceil((canvas.width - panOffset.x) / zoomLevel / gridSize) * gridSize;
const endY = Math.ceil((canvas.height - panOffset.y) / zoomLevel / gridSize) * gridSize;
ctx.strokeStyle = 'rgba(232, 232, 237, 0.5)';
ctx.lineWidth = 0.5;
// 绘制垂直网格线
for (let x = startX; x <= endX; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, startY);
ctx.lineTo(x, endY);
ctx.stroke();
}
// 绘制水平网格线
for (let y = startY; y <= endY; y += gridSize) {
ctx.beginPath();
ctx.moveTo(startX, y);
ctx.lineTo(endX, y);
ctx.stroke();
}
}
// 绘制形状
function drawShape(shape, isSelected) {
ctx.strokeStyle = shape.strokeColor;
ctx.fillStyle = shape.strokeColor;
ctx.lineWidth = shape.lineWidth;
// 绘制选中状态
if (isSelected) {
ctx.strokeStyle = '#0066CC';
ctx.lineWidth = shape.lineWidth + 1;
}
switch (shape.type) {
case 'line':
ctx.beginPath();
ctx.moveTo(shape.startX, shape.startY);
ctx.lineTo(shape.endX, shape.endY);
ctx.stroke();
if (isSelected) {
drawSelectionHandles(shape);
}
break;
case 'rectangle':
ctx.beginPath();
ctx.rect(shape.x, shape.y, shape.width, shape.height);
ctx.stroke();
if (isSelected) {
drawSelectionHandles(shape);
}
break;
case 'circle':
ctx.beginPath();
ctx.arc(shape.centerX, shape.centerY, shape.radius, 0, Math.PI * 2);
ctx.stroke();
if (isSelected) {
drawSelectionHandles(shape);
}
break;
case 'text':
ctx.font = `${shape.fontSize}px -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif`;
ctx.fillText(shape.content, shape.x, shape.y);
if (isSelected) {
ctx.strokeStyle = '#0066CC';
ctx.lineWidth = 1;
const textWidth = ctx.measureText(shape.content).width;
ctx.beginPath();
ctx.rect(shape.x - 2, shape.y - shape.fontSize, textWidth + 4, shape.fontSize + 4);
ctx.stroke();
}
break;
case 'arc':
ctx.beginPath();
ctx.arc(shape.centerX, shape.centerY, shape.radius, shape.startAngle, shape.endAngle);
ctx.stroke();
if (isSelected) {
drawSelectionHandles(shape);
}
break;
case 'polygon':
if (shape.points.length > 1) {
ctx.beginPath();
ctx.moveTo(shape.points[0][0], shape.points[0][1]);
for (let i = 1; i < shape.points.length; i++) {
ctx.lineTo(shape.points[i][0], shape.points[i][1]);
}
ctx.closePath();
ctx.stroke();
if (isSelected) {
drawSelectionHandles(shape);
}
}
break;
case 'dimension':
drawDimension(shape);
if (isSelected) {
drawSelectionHandles(shape);
}
break;
case 'machine-component':
drawMachineComponent(shape);
if (isSelected) {
drawSelectionHandles(shape);
}
break;
}
}
// 绘制尺寸标注
function drawDimension(shape) {
const { startX, startY, endX, endY, value, strokeColor, lineWidth } = shape;
// 计算线的角度和长度
const dx = endX - startX;
const dy = endY - startY;
const angle = Math.atan2(dy, dx);
const length = Math.sqrt(dx * dx + dy * dy);
// 绘制主线
ctx.strokeStyle = strokeColor;
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.stroke();
// 绘制箭头
const arrowSize = 10;
// 起点箭头
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(
startX + arrowSize * Math.cos(angle + Math.PI * 0.85),
startY + arrowSize * Math.sin(angle + Math.PI * 0.85)
);
ctx.moveTo(startX, startY);
ctx.lineTo(
startX + arrowSize * Math.cos(angle - Math.PI * 0.85),
startY + arrowSize * Math.sin(angle - Math.PI * 0.85)
);
ctx.stroke();
// 终点箭头
ctx.beginPath();
ctx.moveTo(endX, endY);
ctx.lineTo(
endX + arrowSize * Math.cos(angle + Math.PI + Math.PI * 0.85),
endY + arrowSize * Math.sin(angle + Math.PI + Math.PI * 0.85)
);
ctx.moveTo(endX, endY);
ctx.lineTo(
endX + arrowSize * Math.cos(angle + Math.PI - Math.PI * 0.85),
endY + arrowSize * Math.sin(angle + Math.PI - Math.PI * 0.85)
);
ctx.stroke();
// 绘制尺寸文本
ctx.font = '12px -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif';
const textWidth = ctx.measureText(`${value}`).width;
// 文本位置
const centerX = (startX + endX) / 2;
const centerY = (startY + endY) / 2;
// 创建文本背景
ctx.fillStyle = 'white';
ctx.fillRect(centerX - textWidth / 2 - 3, centerY - 8, textWidth + 6, 16);
// 绘制文本
ctx.fillStyle = strokeColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${value}`, centerX, centerY);
ctx.textAlign = 'start';
ctx.textBaseline = 'alphabetic';
}
// 绘制机床组件
function drawMachineComponent(shape) {
const { x, y, width, height, componentType, strokeColor, lineWidth } = shape;
ctx.strokeStyle = strokeColor;
ctx.lineWidth = lineWidth;
switch (componentType) {
case 'machine-bed':
// 绘制机床床身
ctx.beginPath();
ctx.rect(x, y, width, height);
ctx.stroke();
// 绘制床身细节
ctx.beginPath();
ctx.moveTo(x, y + height / 4);
ctx.lineTo(x + width, y + height / 4);
ctx.moveTo(x, y + height * 3 / 4);
ctx.lineTo(x + width, y + height * 3 / 4);
ctx.stroke();
break;
case 'machine-column':
// 绘制立柱
ctx.beginPath();
ctx.rect(x, y, width, height);
ctx.stroke();
// 绘制立柱细节
ctx.beginPath();
ctx.moveTo(x + width / 4, y);
ctx.lineTo(x + width / 4, y + height);
ctx.moveTo(x + width * 3 / 4, y);
ctx.lineTo(x + width * 3 / 4, y + height);
ctx.stroke();
break;
case 'machine-spindle':
// 绘制主轴箱
ctx.beginPath();
ctx.rect(x, y, width, height);
ctx.stroke();
// 绘制主轴
ctx.beginPath();
ctx.arc(x + width / 2, y + height / 2, width / 4, 0, Math.PI * 2);
ctx.stroke();
break;
// 可以继续添加其他机床组件的绘制方法
case 'linear-guide':
// 绘制直线导轨
ctx.beginPath();
ctx.rect(x, y, width, height / 4);
ctx.stroke();
ctx.beginPath();
ctx.rect(x + width / 4, y + height / 4, width / 2, height / 2);
ctx.stroke();
break;
case 'ball-screw':
// 绘制滚珠丝杠
ctx.beginPath();
ctx.rect(x, y, width, height / 6);
ctx.stroke();
// 绘制丝杠
ctx.beginPath();
ctx.moveTo(x, y + height / 12);
ctx.lineTo(x + width, y + height / 12);
// 绘制丝杠螺纹
const step = 5;
for (let i = x; i < x + width; i += step) {
ctx.moveTo(i, y + height / 12 - 3);
ctx.lineTo(i + step / 2, y + height / 12 + 3);
}
ctx.stroke();
break;
case 'servo-motor':
// 绘制伺服电机
ctx.beginPath();
ctx.arc(x + width / 2, y + height / 2, width / 2, 0, Math.PI * 2);
ctx.stroke();
// 绘制电机轴
ctx.beginPath();
ctx.moveTo(x + width / 2, y);
ctx.lineTo(x + width / 2, y + height);
ctx.moveTo(x, y + height / 2);
ctx.lineTo(x + width, y + height / 2);
ctx.stroke();
break;
}
}
// 绘制选择框
function drawSelectionHandles(shape) {
const handleSize = 6 / zoomLevel;
ctx.fillStyle = '#0066CC';
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 1 / zoomLevel;
switch (shape.type) {
case 'line':
// 绘制线条两端的控制点
ctx.beginPath();
ctx.rect(shape.startX - handleSize / 2, shape.startY - handleSize / 2, handleSize, handleSize);
ctx.rect(shape.endX - handleSize / 2, shape.endY - handleSize / 2, handleSize, handleSize);
ctx.fill();
ctx.stroke();
break;
case 'rectangle':
// 绘制矩形四角的控制点
const x = shape.x;
const y = shape.y;
const w = shape.width;
const h = shape.height;
ctx.beginPath();
ctx.rect(x - handleSize / 2, y - handleSize / 2, handleSize, handleSize);
ctx.rect(x + w - handleSize / 2, y - handleSize / 2, handleSize, handleSize);
ctx.rect(x - handleSize / 2, y + h - handleSize / 2, handleSize, handleSize);
ctx.rect(x + w - handleSize / 2, y + h - handleSize / 2, handleSize, handleSize);
ctx.fill();
ctx.stroke();
break;
case 'circle':
// 绘制圆形中心和半径点的控制点
ctx.beginPath();
ctx.rect(shape.centerX - handleSize / 2, shape.centerY - handleSize / 2, handleSize, handleSize);
const radiusPoint = {
x: shape.centerX + shape.radius,
y: shape.centerY
};
ctx.rect(radiusPoint.x - handleSize / 2, radiusPoint.y - handleSize / 2, handleSize, handleSize);
ctx.fill();
ctx.stroke();
break;
case 'text':
// 文本的控制点已在绘制函数中处理
break;
case 'arc':
// 绘制弧线的中心点和半径点
ctx.beginPath();
ctx.rect(shape.centerX - handleSize / 2, shape.centerY - handleSize / 2, handleSize, handleSize);
const arcEndPoint = {
x: shape.centerX + shape.radius * Math.cos(shape.endAngle),
y: shape.centerY + shape.radius * Math.sin(shape.endAngle)
};
ctx.rect(arcEndPoint.x - handleSize / 2, arcEndPoint.y - handleSize / 2, handleSize, handleSize);
ctx.fill();
ctx.stroke();
break;
case 'polygon':
// 绘制多边形每个顶点的控制点
ctx.beginPath();
shape.points.forEach(point => {
ctx.rect(point[0] - handleSize / 2, point[1] - handleSize / 2, handleSize, handleSize);
});
ctx.fill();
ctx.stroke();
break;
case 'dimension':
// 绘制尺寸标注两端的控制点
ctx.beginPath();
ctx.rect(shape.startX - handleSize / 2, shape.startY - handleSize / 2, handleSize, handleSize);
ctx.rect(shape.endX - handleSize / 2, shape.endY - handleSize / 2, handleSize, handleSize);
ctx.fill();
ctx.stroke();
break;
case 'machine-component':
// 绘制机床组件四角的控制点
const mx = shape.x;
const my = shape.y;
const mw = shape.width;
const mh = shape.height;
ctx.beginPath();
ctx.rect(mx - handleSize / 2, my - handleSize / 2, handleSize, handleSize);
ctx.rect(mx + mw - handleSize / 2, my - handleSize / 2, handleSize, handleSize);
ctx.rect(mx - handleSize / 2, my + mh - handleSize / 2, handleSize, handleSize);
ctx.rect(mx + mw - handleSize / 2, my + mh - handleSize / 2, handleSize, handleSize);
ctx.fill();
ctx.stroke();
break;
}
}
// 移动形状
function moveShape(shape, dx, dy) {
switch (shape.type) {
case 'line':
shape.startX += dx;
shape.startY += dy;
shape.endX += dx;
shape.endY += dy;
break;
case 'rectangle':
shape.x += dx;
shape.y += dy;
break;
case 'circle':
shape.centerX += dx;
shape.centerY += dy;
break;
case 'text':
shape.x += dx;
shape.y += dy;
break;
case 'arc':
shape.centerX += dx;
shape.centerY += dy;
break;
case 'polygon':
shape.points.forEach(point => {
point[0] += dx;
point[1] += dy;
});
break;
case 'dimension':
shape.startX += dx;
shape.startY += dy;
shape.endX += dx;
shape.endY += dy;
break;
case 'machine-component':
shape.x += dx;
shape.y += dy;
break;
}
}
// 在指定位置查找形状
function findShapeAtPosition(x, y) {
// 倒序查找(最后绘制的形状优先选择)
for (let i = shapes.length - 1; i >= 0; i--) {
const shape = shapes[i];
if (isPointInShape(x, y, shape)) {
return shape;
}
}
return null;
}
// 检查点是否在形状内
function isPointInShape(x, y, shape) {
const tolerance = 5; // 选择容差
switch (shape.type) {
case 'line':
return isPointNearLine(x, y, shape.startX, shape.startY, shape.endX, shape.endY, tolerance);
case 'rectangle':
return x >= shape.x && x <= shape.x + shape.width &&
y >= shape.y && y <= shape.y + shape.height;
case 'circle':
const dx = x - shape.centerX;
const dy = y - shape.centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
return Math.abs(distance - shape.radius) <= tolerance;
case 'text':
const textWidth = ctx.measureText(shape.content).width;
return x >= shape.x && x <= shape.x + textWidth &&
y <= shape.y && y >= shape.y - shape.fontSize;
case 'arc':
const dxArc = x - shape.centerX;
const dyArc = y - shape.centerY;
const distanceArc = Math.sqrt(dxArc * dxArc + dyArc * dyArc);
const angle = Math.atan2(dyArc, dxArc);
// 检查点到圆心的距离是否接近弧的半径
const isNearRadius = Math.abs(distanceArc - shape.radius) <= tolerance;
// 检查点的角度是否在弧的角度范围内
let startAngle = shape.startAngle;
let endAngle = shape.endAngle;
// 确保角度是正的并且终止角度大于起始角度
if (endAngle < startAngle) {
endAngle += Math.PI * 2;
}
let currentAngle = angle;
if (currentAngle < startAngle) {
currentAngle += Math.PI * 2;
}
return isNearRadius && currentAngle >= startAngle && currentAngle <= endAngle;
case 'polygon':
if (shape.points.length < 3) return false;
// 先检查是否接近任何边
for (let i = 0; i < shape.points.length; i++) {
const p1 = shape.points[i];
const p2 = shape.points[(i + 1) % shape.points.length];
if (isPointNearLine(x, y, p1[0], p1[1], p2[0], p2[1], tolerance)) {
return true;
}
}
// 点在多边形内法
return isPointInPolygon(x, y, shape.points);
case 'dimension':
return isPointNearLine(x, y, shape.startX, shape.startY, shape.endX, shape.endY, tolerance);
case 'machine-component':
return x >= shape.x && x <= shape.x + shape.width &&
y >= shape.y && y <= shape.y + shape.height;
}
return false;
}
// 检查点是否接近线段
function isPointNearLine(x, y, x1, y1, x2, y2, tolerance) {
const lineLength = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
if (lineLength === 0) return false;
const t = ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)) / (lineLength * lineLength);
if (t < 0) {
// 最近的点在线段外,靠近点1
const dist = Math.sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1));
return dist <= tolerance;
}
if (t > 1) {
// 最近的点在线段外,靠近点2
const dist = Math.sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
return dist <= tolerance;
}
// 最近点在线段上
const projX = x1 + t * (x2 - x1);
const projY = y1 + t * (y2 - y1);
const dist = Math.sqrt((x - projX) * (x - projX) + (y - projY) * (y - projY));
return dist <= tolerance;
}
// 点是否在多边形内
function isPointInPolygon(x, y, points) {
let inside = false;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
const xi = points[i][0], yi = points[i][1];
const xj = points[j][0], yj = points[j][1];
const intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
// 设置当前工具
function setActiveTool(tool) {
// 更新当前工具
currentTool = tool;
// 如果是选择多边形工具,则重置临时形状
if (tool !== 'polygon') {
tempShape = null;
}
// 更新工具按钮高亮状态
document.querySelectorAll('.cd-tool-btn[data-tool]').forEach(button => {
if (button.getAttribute('data-tool') === tool) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
});
// 更新鼠标光标样式
if (tool === 'select') {
canvas.style.cursor = 'default';
} else {
canvas.style.cursor = 'crosshair';
}
}
// 更新属性面板
function updatePropertiesPanel() {
const propertiesPanel = document.getElementById('propertiesPanel');
if (!selectedShape) {
propertiesPanel.innerHTML = '<div class="cd-empty-state">请选择一个对象以查看其属性</div>';
return;
}
// 创建属性面板内容
let html = '';
// 通用属性
html += `
<div class="cd-property-item">
<label>ID</label>
<input type="text" class="cd-property-value" value="${selectedShape.id}" disabled>
</div>
<div class="cd-property-item">
<label>类型</label>
<input type="text" class="cd-property-value" value="${getShapeTypeName(selectedShape)}" disabled>
</div>
<div class="cd-property-item">
<label>线条颜色</label>
<input type="color" class="cd-property-value" value="${selectedShape.strokeColor}" data-property="strokeColor">
</div>
<div class="cd-property-item">
<label>线条宽度</label>
<select class="cd-property-value" data-property="lineWidth">
<option value="1" ${selectedShape.lineWidth === 1 ? 'selected' : ''}>1px</option>
<option value="2" ${selectedShape.lineWidth === 2 ? 'selected' : ''}>2px</option>
<option value="3" ${selectedShape.lineWidth === 3 ? 'selected' : ''}>3px</option>
<option value="4" ${selectedShape.lineWidth === 4 ? 'selected' : ''}>4px</option>
</select>
</div>
`;
// 特定形状属性
switch (selectedShape.type) {
case 'line':
html += `
<div class="cd-property-item">
<label>起点 X</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.startX)}" data-property="startX">
</div>
<div class="cd-property-item">
<label>起点 Y</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.startY)}" data-property="startY">
</div>
<div class="cd-property-item">
<label>终点 X</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.endX)}" data-property="endX">
</div>
<div class="cd-property-item">
<label>终点 Y</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.endY)}" data-property="endY">
</div>
`;
break;
case 'rectangle':
html += `
<div class="cd-property-item">
<label>X 坐标</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.x)}" data-property="x">
</div>
<div class="cd-property-item">
<label>Y 坐标</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.y)}" data-property="y">
</div>
<div class="cd-property-item">
<label>宽度</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.width)}" data-property="width">
</div>
<div class="cd-property-item">
<label>高度</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.height)}" data-property="height">
</div>
`;
break;
case 'circle':
html += `
<div class="cd-property-item">
<label>中心 X</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.centerX)}" data-property="centerX">
</div>
<div class="cd-property-item">
<label>中心 Y</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.centerY)}" data-property="centerY">
</div>
<div class="cd-property-item">
<label>半径</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.radius)}" data-property="radius">
</div>
`;
break;
case 'text':
html += `
<div class="cd-property-item">
<label>X 坐标</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.x)}" data-property="x">
</div>
<div class="cd-property-item">
<label>Y 坐标</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.y)}" data-property="y">
</div>
<div class="cd-property-item">
<label>文本内容</label>
<input type="text" class="cd-property-value" value="${selectedShape.content}" data-property="content">
</div>
<div class="cd-property-item">
<label>字体大小</label>
<select class="cd-property-value" data-property="fontSize">
<option value="12" ${selectedShape.fontSize === 12 ? 'selected' : ''}>12px</option>
<option value="14" ${selectedShape.fontSize === 14 ? 'selected' : ''}>14px</option>
<option value="16" ${selectedShape.fontSize === 16 ? 'selected' : ''}>16px</option>
<option value="18" ${selectedShape.fontSize === 18 ? 'selected' : ''}>18px</option>
<option value="20" ${selectedShape.fontSize === 20 ? 'selected' : ''}>20px</option>
</select>
</div>
`;
break;
case 'machine-component':
html += `
<div class="cd-property-item">
<label>组件类型</label>
<input type="text" class="cd-property-value" value="${getMachineComponentName(selectedShape.componentType)}" disabled>
</div>
<div class="cd-property-item">
<label>X 坐标</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.x)}" data-property="x">
</div>
<div class="cd-property-item">
<label>Y 坐标</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.y)}" data-property="y">
</div>
<div class="cd-property-item">
<label>宽度</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.width)}" data-property="width">
</div>
<div class="cd-property-item">
<label>高度</label>
<input type="number" class="cd-property-value" value="${Math.round(selectedShape.height)}" data-property="height">
</div>
`;
break;
}
// 添加删除按钮
html += `
<div class="cd-property-item">
<button class="cd-btn cd-btn-primary" id="deleteShapeBtn">删除对象</button>
</div>
`;
propertiesPanel.innerHTML = html;
// 添加属性变更事件监听器
document.querySelectorAll('.cd-property-value[data-property]').forEach(input => {
input.addEventListener('change', function() {
const property = this.getAttribute('data-property');
let value = this.value;
// 将数值型属性转换为数字
if (['startX', 'startY', 'endX', 'endY', 'x', 'y', 'width', 'height', 'centerX', 'centerY', 'radius', 'lineWidth', 'fontSize'].includes(property)) {
value = parseFloat(value);
}
selectedShape[property] = value;
render();
});
});
// 添加删除按钮事件监听器
document.getElementById('deleteShapeBtn').addEventListener('click', function() {
deleteSelectedShape();
});
}
// 更新选中形状的属性
function updateSelectedShapeProperties() {
if (!selectedShape) return;
selectedShape.strokeColor = config.strokeColor;
selectedShape.lineWidth = config.lineWidth;
render();
updatePropertiesPanel();
}
// 删除选中的形状
function deleteSelectedShape() {
if (!selectedShape) return;
const index = shapes.findIndex(s => s.id === selectedShape.id);
if (index !== -1) {
shapes.splice(index, 1);
selectedShape = null;
updatePropertiesPanel();
render();
}
}
// 获取形状类型名称
function getShapeTypeName(shape) {
const typeMap = {
'line': '直线',
'rectangle': '矩形',
'circle': '圆形',
'text': '文本',
'arc': '弧线',
'polygon': '多边形',
'dimension': '尺寸标注',
'machine-component': '机床组件'
};
return typeMap[shape.type] || shape.type;
}
// 获取机床组件名称
function getMachineComponentName(componentType) {
const componentMap = {
'machine-bed': '床身',
'machine-column': '立柱',
'machine-spindle': '主轴箱',
'linear-guide': '直线导轨',
'ball-screw': '滚珠丝杠',
'servo-motor': '伺服电机',
'control-panel': '控制面板',
'electrical-cabinet': '电气柜',
'limit-switch': '限位开关'
};
return componentMap[componentType] || componentType;
}
// 拖放相关函数
function handleDragStart(e) {
const componentType = e.target.getAttribute('data-type');
e.dataTransfer.setData('text/plain', componentType);
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
}
function handleDrop(e) {
e.preventDefault();
const componentType = e.dataTransfer.getData('text/plain');
const pos = getCanvasCoordinates(e);
// 创建对应的机床组件
if (ShapeFactory[componentType]) {
const shape = ShapeFactory[componentType](pos.x, pos.y, {
strokeColor: config.strokeColor,
lineWidth: config.lineWidth
});
shapes.push(shape);
selectedShape = shape;
updatePropertiesPanel();
render();
}
}
// 折叠所有类别
function collapseAllCategories() {
document.querySelectorAll('.cd-category-header').forEach(header => {
header.classList.add('collapsed');
const items = header.nextElementSibling;
items.style.display = 'none';
});
}
// 组件搜索过滤
function filterComponents() {
const searchText = document.getElementById('componentSearch').value.toLowerCase();
document.querySelectorAll('.cd-component-item').forEach(item => {
const text = item.textContent.toLowerCase();
const category = item.closest('.cd-component-category');
const categoryHeader = category.querySelector('.cd-category-header');
const categoryItems = category.querySelector('.cd-category-items');
if (text.includes(searchText)) {
item.style.display = 'block';
categoryHeader.classList.remove('collapsed');
categoryItems.style.display = 'block';
} else {
item.style.display = 'none';
}
});
// 如果类别下所有项目都被隐藏,则隐藏整个类别
document.querySelectorAll('.cd-component-category').forEach(category => {
const items = Array.from(category.querySelectorAll('.cd-component-item'));
const allHidden = items.every(item => item.style.display === 'none');
if (allHidden) {
category.style.display = 'none';
} else {
category.style.display = 'block';
}
});
}
// 文本工具相关函数
function openTextModal(x, y) {
const modal = document.getElementById('textModal');
modal.classList.add('active');
// 存储位置信息
modal.dataset.posX = x;
modal.dataset.posY = y;
// 重置表单
document.getElementById('textContent').value = '';
document.getElementById('fontSize').value = '14';
// 聚焦文本输入框
document.getElementById('textContent').focus();
}
function closeTextModal() {
const modal = document.getElementById('textModal');
modal.classList.remove('active');
}
function confirmTextInput() {
const modal = document.getElementById('textModal');
const x = parseFloat(modal.dataset.posX);
const y = parseFloat(modal.dataset.posY);
const content = document.getElementById('textContent').value;
const fontSize = parseInt(document.getElementById('fontSize').value);
if (content) {
const textShape = ShapeFactory.text(x, y, content, {
fontSize: fontSize,
strokeColor: config.strokeColor
});
shapes.push(textShape);
render();
}
closeTextModal();
}
// 获取Canvas坐标
function getCanvasCoordinates(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
x: (e.clientX - rect.left) * scaleX / zoomLevel - panOffset.x / zoomLevel,
y: (e.clientY - rect.top) * scaleY / zoomLevel - panOffset.y / zoomLevel
};
}
// 清空画布
function clearCanvas() {
if (confirm('确定要清空画布吗?这将删除所有图形。')) {
shapes = [];
selectedShape = null;
tempShape = null;
panOffset = { x: 0, y: 0 };
zoomLevel = 1;
updatePropertiesPanel();
updateZoomDisplay();
render();
}
}
// 导出设计
function exportDesign() {
// 保存当前状态
const currentPanOffset = { ...panOffset };
const currentZoomLevel = zoomLevel;
const currentSelectedShape = selectedShape;
const currentTempShape = tempShape;
// 重置视图以便导出
panOffset = { x: 0, y: 0 };
zoomLevel = 1;
selectedShape = null;
tempShape = null;
// 重新渲染用于导出
render();
// 创建临时画布以绘制白色背景
const exportCanvas = document.createElement('canvas');
exportCanvas.width = canvas.width;
exportCanvas.height = canvas.height;
const exportCtx = exportCanvas.getContext('2d');
// 绘制白色背景
exportCtx.fillStyle = 'white';
exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
// 复制当前画布内容
exportCtx.drawImage(canvas, 0, 0);
// 创建下载链接
const link = document.createElement('a');
link.download = '机床CAD设计_' + new Date().toISOString().slice(0, 10) + '.png';
// 转换为图片URL
exportCanvas.toBlob(function(blob) {
link.href = URL.createObjectURL(blob);
link.click();
// 清理
URL.revokeObjectURL(link.href);
// 恢复原始状态
panOffset = currentPanOffset;
zoomLevel = currentZoomLevel;
selectedShape = currentSelectedShape;
tempShape = currentTempShape;
render();
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', init);
// 监听浏览器调整大小事件
window.addEventListener('resize', function() {
if (canvas) {
const container = canvas.parentElement;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
// 设置 Canvas 的样式属性以适应容器
canvas.style.width = '100%';
canvas.style.height = '100%';
render();
}
});