修复Windows编码问题 - 添加编码安全版本和requirements.txt
|
@ -1,8 +1,17 @@
|
|||
import socket
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# 设置标准输出编码为 UTF-8(解决 Windows 控制台编码问题)
|
||||
if sys.platform.startswith('win'):
|
||||
import codecs
|
||||
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.detach())
|
||||
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.detach())
|
||||
# 设置标准输入编码
|
||||
sys.stdin = codecs.getreader('utf-8')(sys.stdin.detach())
|
||||
|
||||
class JSONSocketClient:
|
||||
def __init__(self, host='localhost', port=8888):
|
||||
self.host = host
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Windows 控制台编码修复模块
|
||||
用于解决 Windows 下 Unicode 字符显示问题
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
def setup_console_encoding():
|
||||
"""设置控制台编码为 UTF-8"""
|
||||
if sys.platform.startswith('win'):
|
||||
try:
|
||||
# 方法1: 设置环境变量
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
|
||||
# 方法2: 重新配置标准流
|
||||
import codecs
|
||||
import io
|
||||
|
||||
# 检查是否已经是正确的编码
|
||||
if hasattr(sys.stdout, 'encoding') and sys.stdout.encoding.lower() not in ['utf-8', 'utf8']:
|
||||
# 只有在需要时才重新配置
|
||||
if hasattr(sys.stdout, 'detach'):
|
||||
sys.stdout = io.TextIOWrapper(
|
||||
sys.stdout.detach(),
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
sys.stderr = io.TextIOWrapper(
|
||||
sys.stderr.detach(),
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
if hasattr(sys.stdin, 'detach'):
|
||||
sys.stdin = io.TextIOWrapper(
|
||||
sys.stdin.detach(),
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not set UTF-8 encoding: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def safe_print(text, fallback_text=None):
|
||||
"""安全打印,如果遇到编码错误则使用备用文本"""
|
||||
try:
|
||||
print(text)
|
||||
except UnicodeEncodeError:
|
||||
if fallback_text:
|
||||
try:
|
||||
print(fallback_text)
|
||||
except UnicodeEncodeError:
|
||||
print("Message with special characters (encoding issue)")
|
||||
else:
|
||||
# 移除非ASCII字符的简单版本
|
||||
ascii_text = text.encode('ascii', errors='ignore').decode('ascii')
|
||||
print(ascii_text if ascii_text.strip() else "Message (encoding issue)")
|
||||
|
||||
def get_safe_emoji_text(emoji_text, fallback_text):
|
||||
"""获取安全的emoji文本,如果不支持则返回备用文本"""
|
||||
try:
|
||||
# 测试是否能编码
|
||||
emoji_text.encode(sys.stdout.encoding or 'utf-8')
|
||||
return emoji_text
|
||||
except (UnicodeEncodeError, LookupError):
|
||||
return fallback_text
|
||||
|
||||
# 预定义的安全文本映射
|
||||
EMOJI_FALLBACKS = {
|
||||
"🚀": "[START]",
|
||||
"📍": "[ADDR]",
|
||||
"⏰": "[TIME]",
|
||||
"✅": "[OK]",
|
||||
"🔗": "[CONN]",
|
||||
"📨": "[MSG]",
|
||||
"❌": "[ERROR]",
|
||||
"🔌": "[DISC]",
|
||||
"📋": "[LIST]",
|
||||
"🖥️": "[SERVER]",
|
||||
"💻": "[CLIENT]",
|
||||
"📁": "[FILE]",
|
||||
"🎯": "[RESULT]",
|
||||
"⚠️": "[WARN]",
|
||||
"📊": "[REPORT]",
|
||||
"🧪": "[TEST]",
|
||||
"🎮": "[INTERACTIVE]",
|
||||
"📤": "[SEND]",
|
||||
"📥": "[RECV]",
|
||||
"👉": "[INPUT]",
|
||||
"👋": "[EXIT]",
|
||||
"🎉": "[SUCCESS]",
|
||||
"🔧": "[FIX]",
|
||||
"⛔": "[STOP]",
|
||||
"💡": "[TIP]"
|
||||
}
|
||||
|
||||
def safe_format_message(message):
|
||||
"""将消息中的emoji替换为安全的文本"""
|
||||
for emoji, fallback in EMOJI_FALLBACKS.items():
|
||||
message = message.replace(emoji, fallback)
|
||||
return message
|
||||
|
||||
# 自动设置编码
|
||||
if __name__ != "__main__":
|
||||
setup_console_encoding()
|
|
@ -0,0 +1,26 @@
|
|||
# Socket JSON 传输系统依赖包列表
|
||||
#
|
||||
# 本项目使用 Python 标准库,无需安装额外包
|
||||
# Python 版本要求: >= 3.6
|
||||
#
|
||||
# 如果您使用的是 Python 3.6+,所有功能都应该正常工作
|
||||
# 如果遇到编码问题,请确保使用支持 UTF-8 的终端
|
||||
|
||||
# 标准库依赖(无需安装):
|
||||
# - socket: 网络通信
|
||||
# - json: JSON 数据处理
|
||||
# - threading: 多线程支持
|
||||
# - subprocess: 进程管理
|
||||
# - datetime: 时间处理
|
||||
# - os: 操作系统接口
|
||||
# - sys: 系统相关功能
|
||||
# - time: 时间功能
|
||||
# - codecs: 编码处理
|
||||
|
||||
# 安装方法(如果需要):
|
||||
# pip install -r requirements.txt
|
||||
|
||||
# 注意: 如果您在 Windows 上遇到编码问题,请:
|
||||
# 1. 使用 Windows Terminal 或 VS Code 终端
|
||||
# 2. 或在 PowerShell/CMD 中运行: chcp 65001
|
||||
# 3. 确保终端支持 UTF-8 编码
|
|
@ -0,0 +1,118 @@
|
|||
require "socket"
|
||||
require "json"
|
||||
|
||||
Sketchup::require "SUWImpl"
|
||||
|
||||
TCP_SERVER_PORT =7999 unless defined?(TCP_SERVER_PORT)
|
||||
OP_CMD_REQ_GETCMDS =0x01 unless defined?(OP_CMD_REQ_GETCMDS)
|
||||
OP_CMD_REQ_SETCMD =0x03 unless defined?(OP_CMD_REQ_SETCMD)
|
||||
OP_CMD_RES_GETCMDS =0x02 unless defined?(OP_CMD_RES_GETCMDS)
|
||||
OP_CMD_RES_SETCMD =0x04 unless defined?(OP_CMD_RES_SETCMD)
|
||||
class SUWclient
|
||||
@@sock =TCPSocket.new("127.0.0.1", TCP_SERVER_PORT)
|
||||
@@seqno=0
|
||||
|
||||
def self.reconnect
|
||||
@@sock.close
|
||||
@@sock=TCPSocket.new("127.0.0.1", TCP_SERVER_PORT)
|
||||
end
|
||||
|
||||
def self.sendmsg(cmd,msg)
|
||||
opcode=(cmd&0xffff)|(0x01010000)
|
||||
@@seqno = @@seqno + 1
|
||||
m=[msg.size(),opcode,@@seqno,0].pack("i*")+msg
|
||||
@@sock.write(m)
|
||||
end
|
||||
|
||||
def self.recvmsg
|
||||
len=@@sock.recv(16).unpack("i*")[0]
|
||||
|
||||
msg=""
|
||||
to_recv_len=len
|
||||
while to_recv_len>0
|
||||
msg+= @@sock.recv(to_recv_len)
|
||||
to_recv_len=len-msg.size()
|
||||
end
|
||||
msg
|
||||
end
|
||||
end
|
||||
|
||||
def get_cmds
|
||||
msg="{\"cmd\":\"get_cmds\",\"params\":{\"from\":\"su\"}}"
|
||||
begin
|
||||
SUWclient.sendmsg(OP_CMD_REQ_GETCMDS,msg)
|
||||
res=SUWclient.recvmsg
|
||||
cmds=[]
|
||||
res_hash=JSON.parse(res)
|
||||
|
||||
if res_hash['ret'] == 1
|
||||
cmds =res_hash['data'].fetch('cmds',[])
|
||||
end
|
||||
|
||||
rescue => e
|
||||
puts "========= get_cmds err is: ========="
|
||||
puts e
|
||||
puts "========= get_cmds res is: ========="
|
||||
puts res
|
||||
SUWclient.reconnect
|
||||
end
|
||||
|
||||
cmds
|
||||
end
|
||||
|
||||
def set_cmd(cmd, params)
|
||||
cmds = {}
|
||||
cmds.store("cmd", "set_cmd")
|
||||
cmds.store("params", params)
|
||||
params.store("from", "su")
|
||||
params.store("cmd", cmd)
|
||||
msg = JSON.generate(cmds)
|
||||
begin
|
||||
SUWclient.sendmsg(OP_CMD_REQ_SETCMD, msg)
|
||||
SUWclient.recvmsg
|
||||
rescue => e
|
||||
puts e
|
||||
SUWclient.reconnect
|
||||
end
|
||||
end
|
||||
|
||||
@cmdsqueue=[]
|
||||
@pause=0
|
||||
$tid = UI.start_timer(1, true){
|
||||
if @pause > 0
|
||||
@pause = @pause -1
|
||||
else
|
||||
swcmds0 = get_cmds
|
||||
swcmds = @cmdsqueue + swcmds0
|
||||
@cmdsqueue.clear
|
||||
#Sketchup.send_action "showRubyPanel:"
|
||||
swcmds.each{|swcmd|
|
||||
data = swcmd.fetch("data")
|
||||
if data.is_a?(String)
|
||||
eval(data)
|
||||
elsif data.is_a?(Hash) && data.key?("cmd")
|
||||
cmd = data.fetch("cmd")
|
||||
puts data
|
||||
if @pause > 0
|
||||
@cmdsqueue << swcmd
|
||||
elsif cmd.start_with? "pause_"
|
||||
@pause= data.fetch("value", 1)
|
||||
else
|
||||
pre_pause_time = data.fetch("pre_pause",0)
|
||||
if pre_pause_time > 0
|
||||
data.delete("pre_pause")
|
||||
swcmd.store("data",data)
|
||||
@pause = pre_pause_time
|
||||
@cmdsqueue<< swcmd
|
||||
else
|
||||
eval("SUWood::SUWimpl.instance.#{cmd}(data)")
|
||||
after_pause_time=data.fetch("after_pause",0)
|
||||
if after_pause_time > 0
|
||||
@pause = after_pause_time
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
}
|
|
@ -0,0 +1,305 @@
|
|||
require 'pathname'
|
||||
|
||||
module SUWood
|
||||
SUSceneNew = 1 #清除之前的订单
|
||||
SUSceneOpen = 2 #清除之前的订单
|
||||
SUSceneSave = 3
|
||||
SUScenePrice = 4
|
||||
|
||||
SUUnitPoint = 11
|
||||
SUUnitFace = 12
|
||||
SUUnitDelete = 13
|
||||
SUUnitContour = 14
|
||||
|
||||
SUZoneFront = 20
|
||||
SUZoneDiv1 = 21
|
||||
SUZoneResize = 22
|
||||
SUZoneCombine = 23
|
||||
SUZoneReplace = 24
|
||||
SUZoneMaterial = 25
|
||||
SUZoneHandle = 26
|
||||
SUZoneCloth = 27
|
||||
SUZoneLight = 28
|
||||
|
||||
VSSpatialPos_F = 1 #前
|
||||
VSSpatialPos_K = 2 #后
|
||||
VSSpatialPos_L = 3 #左
|
||||
VSSpatialPos_R = 4 #右
|
||||
VSSpatialPos_B = 5 #底
|
||||
VSSpatialPos_T = 6 #顶
|
||||
|
||||
VSUnitCont_Zone = 1 #区域轮廓
|
||||
VSUnitCont_Part = 2 #部件轮廓
|
||||
VSUnitCont_Work = 3 #挖洞轮廓
|
||||
|
||||
V_Dealer = 1000
|
||||
V_Machining = 1100
|
||||
V_Division = 1200
|
||||
V_PartCategory = 1300
|
||||
V_Contour = 1400
|
||||
V_Color = 1500
|
||||
V_Profile = 1600
|
||||
V_Surf = 1700
|
||||
V_StretchPart = 1800
|
||||
V_Material = 1900
|
||||
V_Connection = 2000
|
||||
V_HardwareSchema = 2050
|
||||
V_HardwareSet = 2100
|
||||
V_Hardware = 2200
|
||||
V_Groove = 2300
|
||||
V_DesignParam = 2400
|
||||
V_ProfileSchema = 2500
|
||||
V_StructPart = 2600
|
||||
V_CraftPart = 2700
|
||||
V_SeriesPart = 2800
|
||||
V_Drawer = 2900
|
||||
V_DesignTemplate = 3000
|
||||
V_PriceTemplate = 3100
|
||||
V_MachineCut = 3200
|
||||
V_MachineCNC = 3300
|
||||
V_CorpLabel = 3400
|
||||
V_CorpCAM = 3500
|
||||
V_PackLabel = 3600
|
||||
V_Unit = 5000
|
||||
|
||||
PATH = File.dirname(__FILE__)
|
||||
|
||||
module_function
|
||||
def icon_path(icon_name, ext = 'png')
|
||||
"#{PATH}/icon/#{icon_name}.#{ext}"
|
||||
end
|
||||
|
||||
def unit_path()
|
||||
"#{SUWimpl.server_path}/drawings/Unit"
|
||||
end
|
||||
|
||||
def suwood_path(ref_v)
|
||||
case ref_v
|
||||
when V_Material
|
||||
"#{SUWimpl.server_path}/images/texture"
|
||||
when V_StretchPart
|
||||
"#{SUWimpl.server_path}/drawings/StretchPart"
|
||||
when V_StructPart
|
||||
"#{SUWimpl.server_path}/drawings/StructPart"
|
||||
when V_Unit
|
||||
"#{SUWimpl.server_path}/drawings/Unit"
|
||||
when V_Connection
|
||||
"#{SUWimpl.server_path}/drawings/Connection"
|
||||
when V_HardwareSet
|
||||
"#{SUWimpl.server_path}/drawings/HardwareSet"
|
||||
when V_Hardware
|
||||
"#{SUWimpl.server_path}/drawings/Hardware"
|
||||
else
|
||||
"#{SUWimpl.server_path}"
|
||||
end
|
||||
end
|
||||
|
||||
def suwood_pull_size(pos)
|
||||
case pos
|
||||
when 1 then "HW" #右上
|
||||
when 2 then "W" #右中
|
||||
when 3 then "HW" #右下
|
||||
when 4 then "H" #中上
|
||||
when 6 then "H" #中下
|
||||
when 11 then "HW" #右上-竖
|
||||
when 12 then "W" #右中-竖
|
||||
when 13 then "HW" #右下-竖
|
||||
when 14 then "H" #中上-竖
|
||||
when 16 then "H" #中下-竖
|
||||
when 21 then "HW" #右上-横
|
||||
when 22 then "W" #右中-横
|
||||
when 23 then "HW" #右下-横
|
||||
when 24 then "H" #中上-横
|
||||
when 26 then "H" #中下-横
|
||||
else nil
|
||||
end
|
||||
end
|
||||
|
||||
def scene_save()
|
||||
at_model = Sketchup.active_model
|
||||
order_id = at_model.get_attribute("sw", "order_id", nil)
|
||||
return if order_id.nil?
|
||||
|
||||
data = {}
|
||||
data.store("method", SUSceneSave)
|
||||
data.store("order_id", order_id)
|
||||
set_cmd("r00", data)
|
||||
|
||||
if at_model.path.empty?
|
||||
scene_path = Pathname.new("#{SUWimpl.server_path}/sketchup").expand_path
|
||||
unless Dir.exist?(scene_path)
|
||||
Dir.mkdir(scene_path)
|
||||
end
|
||||
order_code = at_model.get_attribute("sw", "order_code")
|
||||
at_model.save("#{scene_path}/#{order_code}.skp")
|
||||
else
|
||||
at_model.save
|
||||
end
|
||||
end
|
||||
|
||||
def scene_price()
|
||||
at_model = Sketchup.active_model
|
||||
order_id = at_model.get_attribute("sw", "order_id", nil)
|
||||
return if order_id.nil?
|
||||
|
||||
params = {}
|
||||
params.store("method", SUScenePrice)
|
||||
params.store("order_id", order_id)
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
def import_unit(uid, values, mold)#点击创体(产品UID)
|
||||
Sketchup.active_model.select_tool(SUWUnitPointTool.new(values["width"].mm, values["depth"].mm, values["height"].mm, uid, mold))
|
||||
end
|
||||
|
||||
def import_face(uid, values, mold)#选面创体(产品UID)
|
||||
Sketchup.active_model.select_tool(SUWUnitFaceTool.new(VSSpatialPos_F, uid, mold))
|
||||
end
|
||||
|
||||
def front_view()
|
||||
uid = SUWimpl.selected_uid
|
||||
obj = SUWimpl.selected_obj
|
||||
if uid.nil? || obj.nil?
|
||||
UI.messagebox("请先选择正视于的基准面!")
|
||||
return
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("method", SUZoneFront)
|
||||
params.store("uid", uid)
|
||||
params.store("oid", obj)
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
def delete_unit()
|
||||
order_id = Sketchup.active_model.get_attribute("sw", "order_id", nil)
|
||||
uid = SUWimpl.selected_uid
|
||||
obj = SUWimpl.selected_obj
|
||||
if uid.nil?
|
||||
UI.messagebox("请先选择待删除的柜体!")
|
||||
return
|
||||
elsif order_id.nil?
|
||||
UI.messagebox("当前柜体不是场景方案的柜体!")
|
||||
return
|
||||
elsif UI.messagebox("是否确定删除当前选择的柜体?", MB_YESNO) == IDNO
|
||||
return
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("method", SUUnitDelete)
|
||||
params.store("order_id", order_id)
|
||||
params.store("uid", uid)
|
||||
params.store("oid", obj) if obj
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
def combine_unit(uid, values, mold)#模块拼接
|
||||
selected_zone = SUWimpl.selected_zone
|
||||
if selected_zone.nil?
|
||||
UI.messagebox("请先选择待拼接的空区域!")
|
||||
return
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("method", SUZoneCombine)
|
||||
params.store("uid", selected_zone.get_attribute("sw","uid"))
|
||||
params.store("zid", selected_zone.get_attribute("sw","zid"))
|
||||
params.store("source", uid)
|
||||
params.store("module", mold) if mold
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
def replace_unit(uid, values, mold)#模块/产品替换
|
||||
if SUWimpl.selected_zone.nil? && (mold == 1 || mold == 2)
|
||||
UI.messagebox("请先选择待替换的区域!"); return
|
||||
elsif SUWimpl.selected_obj.nil? && (mold == 3)
|
||||
UI.messagebox("请先选择待替换的部件!"); return
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("method", SUZoneReplace)
|
||||
params.store("source", uid)
|
||||
params.store("module", mold)
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
def replace_mat(uid, values, type)#材料替换
|
||||
selected_zone = SUWimpl.selected_zone
|
||||
if selected_zone.nil?
|
||||
UI.messagebox("请先选择待替换材料的区域!")
|
||||
return
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("method", SUZoneMaterial)
|
||||
params.store("mat_id", uid)
|
||||
params.store("type", type)
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
def replace_handle(width, height, set_id, conn_id)
|
||||
selected_zone = SUWimpl.selected_zone
|
||||
if selected_zone.nil?
|
||||
UI.messagebox("请先选择待替换拉手的区域!")
|
||||
return
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("method", SUZoneHandle)
|
||||
params.store("uid", selected_zone.get_attribute("sw","uid"))
|
||||
params.store("zid", selected_zone.get_attribute("sw","zid"))
|
||||
params.store("conn_id", conn_id)
|
||||
params.store("set_id", set_id)
|
||||
params.store("width", width.to_i) unless width.nil? || width == ""
|
||||
params.store("width", height.to_i) unless height.nil? || height == ""
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
def clear_current(ref_v)
|
||||
if (ref_v == 2102 || ref_v == 2103) && SUWimpl.selected_zone
|
||||
params = {}#没有柜体/区域参数, 切换到订单编辑界面
|
||||
params.store("uid", SUWimpl.selected_uid)
|
||||
set_cmd("r01", params)
|
||||
SUWimpl.instance.sel_clear
|
||||
end
|
||||
end
|
||||
|
||||
#挂衣杆替换
|
||||
def replace_clothes(front, back, set_id, conn_id)
|
||||
selected_zone = SUWimpl.selected_zone
|
||||
if selected_zone.nil?
|
||||
UI.messagebox("请先选择待替换衣杆的区域!")
|
||||
return
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("method", SUZoneCloth)
|
||||
params.store("uid", selected_zone.get_attribute("sw","uid"))
|
||||
params.store("zid", selected_zone.get_attribute("sw","zid"))
|
||||
params.store("conn_id", conn_id)
|
||||
params.store("set_id", set_id)
|
||||
params.store("front", front) unless front == 0
|
||||
params.store("back", back) unless back == 0
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
#灯带替换
|
||||
def replace_lights(front, back, set_id, conn_id)
|
||||
selected_zone = SUWimpl.selected_zone
|
||||
if selected_zone.nil?
|
||||
UI.messagebox("请先选择待替换灯带的区域!")
|
||||
return
|
||||
end
|
||||
|
||||
conns = conn_id.class == Array ? conn_id.join(",") : conn_id
|
||||
params = {}
|
||||
params.store("method", SUZoneLight)
|
||||
params.store("uid", selected_zone.get_attribute("sw","uid"))
|
||||
params.store("zid", selected_zone.get_attribute("sw","zid"))
|
||||
params.store("conn_id", conns)
|
||||
params.store("set_id", set_id)
|
||||
params.store("front", front) unless front == 0
|
||||
params.store("back", back) unless back == 0
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
||||
|
||||
Sketchup::require('SUWConstants')
|
||||
Sketchup::require('SUWImpl')
|
||||
Sketchup::require('SUWClient')
|
||||
Sketchup::require('SUWObserver')
|
||||
Sketchup::require('SUWUnitPointTool')
|
||||
Sketchup::require('SUWUnitFaceTool')
|
||||
Sketchup::require('SUWUnitContTool')
|
||||
Sketchup::require('SUWZoneDiv1Tool')
|
||||
Sketchup::require('SUWMenu')
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
module SUWood
|
||||
# 创建菜单
|
||||
unless file_loaded?(__FILE__) # 只有加载一次
|
||||
|
||||
SUWimpl.instance.startup
|
||||
Sketchup.break_edges = false
|
||||
Sketchup.active_model.selection.add_observer(SUWSelObserver.new)
|
||||
Sketchup.active_model.tools.add_observer(SUWToolsObserver.new)
|
||||
Sketchup.add_observer(SUWAppObserver.new)
|
||||
UI.add_context_menu_handler{|menu|
|
||||
selection = Sketchup.active_model.selection
|
||||
faces = selection.select{|e| e.class == Sketchup::Face}
|
||||
unless faces.length == 1
|
||||
menu.add_item("创建轮廓") {
|
||||
json = faces[0].to_json
|
||||
if json.empty?
|
||||
UI.messagebox("没有选取图形!")
|
||||
else
|
||||
set_cmd("r02", json)#"create_contour"
|
||||
end
|
||||
}
|
||||
|
||||
if SUWimpl.instance.added_contour
|
||||
menu.add_item("取消轮廓") {
|
||||
SUWimpl.instance.added_contour = false
|
||||
set_cmd("r02", {:segs => []}) #"create_contour"
|
||||
}
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
# root_toolbar = UI.toolbar("SUWood")
|
||||
#
|
||||
# cmd1 = UI::Command.new("点击创体") {
|
||||
# SUWood::SUWUnitPointTool.set_box
|
||||
# }
|
||||
# cmd1.tooltip = '点击创体' # 设置提示文字
|
||||
# cmd1.small_icon = icon_path('unit_point', 'png')
|
||||
# cmd1.large_icon = icon_path('unit_point', 'png')
|
||||
# root_toolbar.add_item(cmd1) # 添加按钮
|
||||
#
|
||||
# cmd2 = UI::Command.new("选面创体") {
|
||||
# Sketchup.active_model.select_tool(SUWood::SUWUnitFaceTool.new)
|
||||
# }
|
||||
# cmd2.tooltip = '选面创体' # 设置提示文字
|
||||
# cmd2.small_icon = icon_path('unit_face', 'png')
|
||||
# cmd2.large_icon = icon_path('unit_face', 'png')
|
||||
# root_toolbar.add_item(cmd2) # 添加按钮
|
||||
#
|
||||
# cmd3 = UI::Command.new('删除柜体'){
|
||||
# SUWood::delete_unit
|
||||
# }
|
||||
# cmd3.small_icon = icon_path('unit_delete', 'png')
|
||||
# cmd3.large_icon = icon_path('unit_delete', 'png')
|
||||
# cmd3.tooltip = '删除柜体'
|
||||
# root_toolbar.add_item(cmd3)
|
||||
#
|
||||
# cmd4 = UI::Command.new('六面切割'){
|
||||
# Sketchup.active_model.select_tool(SUWood::SWZoneDiv1Tool.new)
|
||||
# }
|
||||
# cmd4.small_icon = icon_path('zone_div1', 'png')
|
||||
# cmd4.large_icon = icon_path('zone_div1', 'png')
|
||||
# cmd4.tooltip = '六面切割'
|
||||
# root_toolbar.add_item(cmd4)
|
||||
#
|
||||
# root_toolbar.show # 展示
|
||||
|
||||
file_loaded(__FILE__)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,87 @@
|
|||
module SUWood
|
||||
class SUWToolsObserver < Sketchup::ToolsObserver
|
||||
@@cloned_zone = nil
|
||||
|
||||
def onActiveToolChanged(tools, tool_name, tool_id)
|
||||
#puts "onActiveToolChanged: #{tool_id}\t#{tool_name}"
|
||||
# 21048 = MoveTool
|
||||
# 21129 = RotateTool
|
||||
if tool_id == 21236#ScaleTool
|
||||
SUWimpl.instance.scaled_start
|
||||
else
|
||||
SUWimpl.instance.scaled_finish
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SUWSelObserver < Sketchup::SelectionObserver
|
||||
def onSelectionBulkChange(selection)
|
||||
if selection.length <= 0#1次点击, 多次调用
|
||||
return
|
||||
end
|
||||
|
||||
suw_objs = selection.select{|e|
|
||||
e.valid? && (e.class == Sketchup::Group || e.class == Sketchup::ComponentInstance) && e.get_attribute("sw", "uid", nil)
|
||||
}
|
||||
|
||||
if suw_objs.empty?
|
||||
if Sketchup.active_model.get_attribute("sw", "order_id", nil) && SUWimpl.selected_uid
|
||||
set_cmd("r01", {})#没有柜体/区域参数, 切换到订单编辑界面
|
||||
end
|
||||
|
||||
SUWimpl.instance.sel_clear#清除数据
|
||||
elsif suw_objs.length == 1#每次选择1个SUWood对象, 后续如实现多选则由SUWood自己管理
|
||||
selection.clear
|
||||
SUWimpl.instance.sel_local(suw_objs[0])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#新建1个模型或打开其他模型前, SU提示是否保存当前模型时:
|
||||
#如选择是则保存相应的订单及柜体
|
||||
#如选择否则取消相应订单更改(包含在新建或打开事件中)
|
||||
# FIXME:TODO:草图自动保存时也会调用
|
||||
class SUWModelObserver < Sketchup::ModelObserver
|
||||
def onSaveModel(model)
|
||||
order_id = model.get_attribute("sw", "order_id", nil)
|
||||
return if order_id.nil?
|
||||
|
||||
params = {}
|
||||
params.store("method", SUSceneSave)
|
||||
params.store("order_id", order_id)
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
end
|
||||
|
||||
class SUWAppObserver < Sketchup::AppObserver
|
||||
def onNewModel(model)
|
||||
SUWimpl.instance.startup
|
||||
# model.add_observer(SUWModelObserver.new)
|
||||
model.tools.add_observer(SUWToolsObserver.new)
|
||||
model.selection.add_observer(SUWSelObserver.new)
|
||||
|
||||
params = {}
|
||||
params.store("method", SUSceneNew)
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
def onOpenModel(model)
|
||||
SUWimpl.instance.startup
|
||||
# model.add_observer(SUWModelObserver.new)
|
||||
model.tools.add_observer(SUWToolsObserver.new)
|
||||
model.selection.add_observer(SUWSelObserver.new)
|
||||
|
||||
order_id = model.get_attribute("sw", "order_id", nil)
|
||||
model.entities.each {|entity|
|
||||
if entity.class == Sketchup::Group && entity.valid? && entity.get_attribute("sw", "uid", nil)
|
||||
entity.erase!
|
||||
end
|
||||
} unless order_id.nil?
|
||||
|
||||
params = {}
|
||||
params.store("method", SUSceneOpen)
|
||||
params.store("order_id", order_id) unless order_id.nil?
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,137 @@
|
|||
module SUWood
|
||||
class SUWUnitContTool
|
||||
def self.set_type(cont_type)
|
||||
if cont_type == VSUnitCont_Zone
|
||||
select = SUWimpl.selected_zone
|
||||
if select.nil? || select.deleted?
|
||||
Sketchup::set_status_text('请选择区域'); return
|
||||
end
|
||||
uid = select.get_attribute("sw", "uid")
|
||||
oid = select.get_attribute("sw", "zid")
|
||||
cp = -1
|
||||
else
|
||||
select = SUWimpl.selected_part
|
||||
if select.nil? || select.deleted?
|
||||
Sketchup::set_status_text('请选择部件'); return
|
||||
end
|
||||
uid = select.get_attribute("sw", "uid")
|
||||
oid = select.get_attribute("sw", "pid")
|
||||
cp = select.get_attribute("sw", "cp")
|
||||
end
|
||||
Sketchup.active_model.select_tool(new(cont_type, select, uid, oid, cp))
|
||||
end
|
||||
|
||||
def initialize(cont_type, select, uid, oid, cp = -1)
|
||||
@cont_type, @uid, @oid, @cp = cont_type, uid, oid, cp
|
||||
@select = select
|
||||
if cont_type == VSUnitCont_Zone
|
||||
@tooltip = '请选择区域的面, 并指定对应的轮廓'
|
||||
else#VSUnitCont_Work
|
||||
@tooltip = '请选择板件的面, 并指定对应的轮廓'
|
||||
end
|
||||
end
|
||||
|
||||
def activate
|
||||
Sketchup.set_status_text(@tooltip)
|
||||
end
|
||||
|
||||
def onMouseMove(flags, x, y, view)
|
||||
@ref_face = nil
|
||||
@face_segs = nil
|
||||
ref_face = nil
|
||||
xypicker = view.pick_helper
|
||||
xypicker.do_pick(x, y)
|
||||
xypicker.count.times do |i|
|
||||
path = xypicker.path_at(i)
|
||||
if path[-1].class == Sketchup::Face && !@select.entities.include?(path[-1])
|
||||
ref_face = path[-1]
|
||||
end
|
||||
end
|
||||
if ref_face
|
||||
face_pts = ref_face.outer_loop.vertices.map(&:position)
|
||||
@ref_face = ref_face
|
||||
@face_segs = face_pts.zip(face_pts.rotate)
|
||||
view.invalidate
|
||||
end
|
||||
Sketchup.set_status_text(@tooltip)
|
||||
view.invalidate
|
||||
end
|
||||
|
||||
def onLButtonDown(flags, x, y, view)
|
||||
if @ref_face.nil?
|
||||
UI.messagebox('请选择轮廓')
|
||||
return
|
||||
end
|
||||
|
||||
myself = false
|
||||
depth = 0
|
||||
arced = true
|
||||
case @cont_type
|
||||
when VSUnitCont_Zone then
|
||||
return if UI.messagebox('是否确定创建区域轮廓?', MB_YESNO) == IDNO
|
||||
when VSUnitCont_Part
|
||||
return if UI.messagebox('是否确定创建部件轮廓?', MB_YESNO) == IDNO
|
||||
when VSUnitCont_Work
|
||||
arcs = @ref_face.edges.select{|edge| edge.curve.is_a?(Sketchup::ArcCurve)}
|
||||
if arcs.empty?
|
||||
prompts = ["表面", "深度"]
|
||||
values = ["当前", 0]
|
||||
options = ["当前|反面", ""]
|
||||
inputs = UI.inputbox(prompts, values, options, '挖洞轮廓')
|
||||
return if inputs == false
|
||||
|
||||
myself = inputs[0] == "当前"
|
||||
depth = inputs[1] if inputs[1] > 0
|
||||
else
|
||||
prompts = ["表面", "深度", "圆弧"]
|
||||
values = ["当前", 0, "圆弧"]
|
||||
options = ["当前|反面", "", "圆弧|多段线"]
|
||||
inputs = UI.inputbox(prompts, values, options, '挖洞轮廓')
|
||||
return if inputs == false
|
||||
|
||||
myself = inputs[0] == "当前"
|
||||
depth = inputs[1] if inputs[1] > 0
|
||||
arced = inputs[2] == "圆弧"
|
||||
end
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("method", SUUnitContour)
|
||||
params.store("type", @cont_type)
|
||||
params.store("uid", @uid)
|
||||
params.store("oid", @oid)
|
||||
params.store("cp", @cp)
|
||||
params.store("face", @ref_face.to_json(nil, 1, arced))
|
||||
params.store("self", myself)
|
||||
params.store("depth", depth)
|
||||
set_cmd("r00", params)
|
||||
|
||||
edges = []
|
||||
@ref_face.edges.each {|edge|
|
||||
if edge.faces.length == 1
|
||||
edges << edge
|
||||
end
|
||||
}
|
||||
@ref_face.erase!
|
||||
@ref_face = nil
|
||||
edges.each {|edge|
|
||||
if edge.valid?
|
||||
edge.erase!
|
||||
end
|
||||
}
|
||||
|
||||
@face_segs = nil
|
||||
view.invalidate
|
||||
Sketchup.active_model.selection.clear
|
||||
Sketchup.active_model.select_tool(nil)
|
||||
end
|
||||
|
||||
def draw(view)
|
||||
if @face_segs
|
||||
view.drawing_color = Sketchup::Color.new(0, 255, 255)
|
||||
view.line_width = 3
|
||||
view.draw2d(GL_LINES, @face_segs.flat_map{|seg| seg.map{|pt| view.screen_coords(pt)}})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,145 @@
|
|||
module SUWood
|
||||
class SUWUnitFaceTool
|
||||
def initialize(cont_view, source = nil, mold = false)
|
||||
@cont_view, @source, @mold = cont_view, source, mold
|
||||
@tooltip = '请点击要创体的面'
|
||||
end
|
||||
|
||||
def activate
|
||||
Sketchup.set_status_text(@tooltip)
|
||||
end
|
||||
|
||||
def onMouseMove(flags, x, y, view)
|
||||
@ref_face = nil
|
||||
@trans_arr = nil
|
||||
@face_segs = nil
|
||||
|
||||
pickray = view.pickray(x, y)
|
||||
result = Sketchup.active_model.raytest(pickray)
|
||||
if result#FIXME:TODO:墙面支持圆弧
|
||||
path_arr = result[1]
|
||||
ref_face = path_arr.reverse.find{|e| e.is_a?(Sketchup::Face)}
|
||||
if face_valid?(ref_face)
|
||||
face_pts = ref_face.outer_loop.vertices.map(&:position)
|
||||
face_idx = path_arr.find_index(ref_face)
|
||||
if face_idx > 0
|
||||
trans_arr = path_arr[0..face_idx-1].map(&:transformation).reverse!
|
||||
trans_arr.each{|trans| face_pts.each{|pt| pt.transform!(trans)}}
|
||||
@trans_arr = trans_arr
|
||||
end
|
||||
@ref_face = ref_face
|
||||
@face_segs = face_pts.zip(face_pts.rotate)
|
||||
end
|
||||
end
|
||||
Sketchup.set_status_text(@tooltip)
|
||||
view.invalidate
|
||||
end
|
||||
|
||||
def onLButtonDown(flags, x, y, view)
|
||||
if @ref_face.nil?#当前点击是否选择了face
|
||||
xypicker = view.pick_helper(x, y)
|
||||
ref_face = xypicker.picked_face
|
||||
if face_valid?(ref_face)
|
||||
face_pts = ref_face.outer_loop.vertices.map(&:position)
|
||||
@ref_face = ref_face
|
||||
@face_segs = face_pts.zip(face_pts.rotate)
|
||||
view.invalidate
|
||||
end
|
||||
end
|
||||
|
||||
if @ref_face.nil?
|
||||
UI.messagebox('请选择要放置的面')
|
||||
return
|
||||
end
|
||||
|
||||
caption = ""
|
||||
default = 0
|
||||
case @cont_view
|
||||
when VSSpatialPos_F then caption = '深(mm)'; default = 600
|
||||
when VSSpatialPos_R then caption = '宽(mm)'; default = 800
|
||||
when VSSpatialPos_T then caption = '高(mm)'; default = 800
|
||||
else
|
||||
end
|
||||
prompts = ['距左', '距右', '距上', '距下', caption, "重叠"]
|
||||
values = [0, 0, 0, 0, default, "合并"]
|
||||
options = ["", "", "", "", "", "合并|截除"]
|
||||
inputs = UI.inputbox(prompts, values, options, '选面创体')
|
||||
return if inputs == false || inputs[4] < 100
|
||||
|
||||
order_id = Sketchup.active_model.get_attribute("sw", "order_id", nil)
|
||||
fronts = []
|
||||
if @cont_view == VSSpatialPos_T
|
||||
@ref_face.outer_loop.edges.each{|edge|
|
||||
next if edge.curve
|
||||
faces = edge.faces.select{|face| face.normal.perpendicular?(Z_AXIS)}
|
||||
if faces.empty?
|
||||
p1 = edge.start.position
|
||||
p2 = edge.end.position
|
||||
@trans_arr.each{|trans|
|
||||
p1.transform!(trans)
|
||||
p2.transform!(trans)
|
||||
} if @trans_arr
|
||||
seg = Hash.new
|
||||
seg.store("s", p1.to_s("mm", 1))
|
||||
seg.store("e", p2.to_s("mm", 1))
|
||||
fronts << seg
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("view", @cont_view)
|
||||
params.store("face", @ref_face.to_json(@trans_arr, 1))
|
||||
params.store("left", inputs[0]) if inputs[0] > 0
|
||||
params.store("right", inputs[1]) if inputs[1] > 0
|
||||
params.store("top", inputs[2]) if inputs[2] > 0
|
||||
params.store("bottom", inputs[3]) if inputs[3] > 0
|
||||
params.store("size", inputs[4])
|
||||
params.store("merged", true) if inputs[5] == "合并"
|
||||
params.store("source", @source) unless @source.nil?
|
||||
params.store("module", @mold) if @mold
|
||||
params.store("fronts", fronts) if fronts.length > 0
|
||||
|
||||
data = {}
|
||||
data.store("method", SUUnitFace)
|
||||
data.store("order_id", order_id) unless order_id.nil?
|
||||
data.store("params", params)
|
||||
set_cmd("r00", data)
|
||||
|
||||
edges = []
|
||||
@ref_face.edges.each {|edge|
|
||||
if edge.faces.length == 1
|
||||
edges << edge
|
||||
end
|
||||
}
|
||||
@ref_face.erase!
|
||||
@ref_face = nil
|
||||
edges.each {|edge|
|
||||
if edge.valid?
|
||||
edge.erase!
|
||||
end
|
||||
}
|
||||
|
||||
@trans_arr = nil
|
||||
@face_segs = nil
|
||||
view.invalidate
|
||||
Sketchup.active_model.selection.clear
|
||||
Sketchup.active_model.select_tool(nil)
|
||||
end
|
||||
|
||||
def draw(view)
|
||||
if @face_segs
|
||||
view.drawing_color = Sketchup::Color.new(0, 255, 255)
|
||||
view.line_width = 3
|
||||
view.draw2d(GL_LINES, @face_segs.flat_map{|seg| seg.map{|pt| view.screen_coords(pt)}})
|
||||
end
|
||||
end
|
||||
|
||||
def face_valid?(face)
|
||||
face &&
|
||||
( @cont_view == VSSpatialPos_F && face.normal.perpendicular?(Z_AXIS) ||
|
||||
@cont_view == VSSpatialPos_R && face.normal.perpendicular?(Z_AXIS) ||
|
||||
@cont_view == VSSpatialPos_T && face.normal.parallel?(Z_AXIS))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,128 @@
|
|||
module SUWood
|
||||
class SUWUnitPointTool
|
||||
def self.set_box
|
||||
prompts = ['宽(mm):', '深(mm):', '高(mm):']
|
||||
defaults = [1200, 600, 800]
|
||||
input = UI.inputbox(prompts, defaults, '输入柜体的三维尺寸')
|
||||
return unless input
|
||||
x_len, y_len, z_len = input.map { |n| n.mm.to_f }
|
||||
Sketchup.active_model.select_tool(new(x_len, y_len, z_len))
|
||||
end
|
||||
|
||||
def initialize(x_len, y_len, z_len, source = nil, mold = false)
|
||||
@x_len, @y_len, @z_len, @source, @mold = x_len, y_len, z_len, source, mold
|
||||
@z_rotation, @x_rotation = 0, 0
|
||||
@current_point = ORIGIN
|
||||
front_pts = [ORIGIN]
|
||||
front_pts << Geom::Point3d.new(x_len, 0, 0)
|
||||
front_pts << Geom::Point3d.new(x_len, 0, z_len)
|
||||
front_pts << Geom::Point3d.new(0, 0, z_len)
|
||||
back_vec = Geom::Vector3d.new(0, y_len, 0)
|
||||
back_pts = front_pts.map { |pt| pt.offset(back_vec) }
|
||||
@front_face = front_pts
|
||||
@box_segs = front_pts.zip(back_pts)
|
||||
@box_segs.concat(front_pts.zip(front_pts.rotate))
|
||||
@box_segs.concat(back_pts.zip(back_pts.rotate))
|
||||
@cursor_id = UI.create_cursor(SUWood.icon_path('cursor_move', 'svg'), 10, 10)
|
||||
@tooltip = '按Ctrl键切换柜体朝向'
|
||||
end
|
||||
|
||||
def activate
|
||||
main_window_focus
|
||||
end
|
||||
|
||||
def deactivate(view) end
|
||||
|
||||
def onSetCursor
|
||||
UI.set_cursor(@cursor_id)
|
||||
end
|
||||
|
||||
def onCancel(reason, view)
|
||||
Sketchup.active_model.select_tool(nil)
|
||||
end
|
||||
|
||||
def onMouseMove(flags, x, y, view)
|
||||
@ip = view.inputpoint(x + 10, y - 5)
|
||||
@current_point = @ip.position
|
||||
view.invalidate
|
||||
end
|
||||
|
||||
def onLButtonDown(flags, x, y, view)
|
||||
@ip = view.inputpoint(x + 10, y - 5)
|
||||
@current_point = @ip.position
|
||||
view.invalidate
|
||||
Sketchup.active_model.selection.clear
|
||||
Sketchup.active_model.select_tool(nil)
|
||||
|
||||
order_id = Sketchup.active_model.get_attribute("sw", "order_id", nil)
|
||||
trans = get_current_trans
|
||||
|
||||
params = {}
|
||||
params.store("width", @x_len.to_mm)
|
||||
params.store("depth", @y_len.to_mm)
|
||||
params.store("height", @z_len.to_mm)
|
||||
params.store("source", @source) unless @source.nil?
|
||||
params.store("module", @mold) if @mold
|
||||
trans.store(params)
|
||||
|
||||
data = {}#uid/folder_id为0
|
||||
data.store("method", SUUnitPoint)
|
||||
data.store("order_id", order_id) unless order_id.nil?
|
||||
data.store("params", params)
|
||||
set_cmd("r00", data)
|
||||
end
|
||||
|
||||
def draw(view)
|
||||
@ip.draw(view) if @ip && @ip.valid? && @ip.display?
|
||||
tr = get_current_trans
|
||||
box_segs = @box_segs.map { |seg| seg.map { |pt| pt.transform(tr) } }
|
||||
front_face = @front_face.map { |pt| pt.transform(tr) }
|
||||
|
||||
view.line_width = 1
|
||||
view.drawing_color = Sketchup::Color.new(0, 0, 0)
|
||||
view.draw(GL_LINES, box_segs.flatten(1))
|
||||
|
||||
view.drawing_color = Sketchup::Color.new(255, 0, 0, 0.3)
|
||||
view.draw(GL_QUADS, front_face) # 参考面
|
||||
Sketchup::set_status_text(@tooltip)
|
||||
end
|
||||
|
||||
def getExtents
|
||||
tr = get_current_trans
|
||||
box_segs = @box_segs.map { |seg| seg.map { |pt| pt.transform(tr) } }
|
||||
bb = Geom::BoundingBox.new
|
||||
bb.add(box_segs.flatten)
|
||||
bb.add(@ip.position) if @ip && @ip.valid?
|
||||
bb
|
||||
end
|
||||
|
||||
def onKeyUp(key, rpt, flags, view)
|
||||
case key
|
||||
when VK_CONTROL
|
||||
@z_rotation -= 1
|
||||
view.invalidate
|
||||
end
|
||||
end
|
||||
|
||||
def get_current_trans
|
||||
trans = Geom::Transformation::translation(@current_point)
|
||||
if @z_rotation != 0
|
||||
origin = trans.origin
|
||||
angle = @z_rotation * Math::PI * 0.5
|
||||
trans = Geom::Transformation.rotation(origin, Z_AXIS, angle) * trans
|
||||
end
|
||||
if @x_rotation != 0
|
||||
origin = trans.origin
|
||||
angle = @x_rotation * Math::PI * 0.5
|
||||
trans = Geom::Transformation.rotation(origin, X_AXIS, angle) * trans
|
||||
end
|
||||
trans
|
||||
end
|
||||
|
||||
def main_window_focus
|
||||
dlg = UI::WebDialog.new("shink_mwf", true, "shink_mwf", 0, 0, 10000, 10000, true)
|
||||
dlg.show
|
||||
dlg.close
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,106 @@
|
|||
module SUWood
|
||||
class SWZoneDiv1Tool
|
||||
def initialize
|
||||
@pattern = :up
|
||||
reset_status_text
|
||||
end
|
||||
|
||||
def activate
|
||||
Sketchup.set_status_text(@tooltip)
|
||||
Sketchup.active_model.selection.clear
|
||||
end
|
||||
|
||||
def resume(view)
|
||||
Sketchup.set_status_text(@tooltip)
|
||||
end
|
||||
|
||||
def reset_status_text
|
||||
@tooltip = "选择一个要分割的区域, "
|
||||
if @pattern == :up
|
||||
@tooltip << "按方向键进行上下左右分割"
|
||||
else
|
||||
@tooltip << "按方向键上下进行前后分割"
|
||||
end
|
||||
@tooltip << ", 按ctrl键可切换模式"
|
||||
Sketchup.set_status_text(@tooltip)
|
||||
end
|
||||
|
||||
def divide(dir)
|
||||
selected_zone = SUWimpl.selected_zone
|
||||
if selected_zone.nil?
|
||||
UI.messagebox("请先选择要分割的区域!")
|
||||
return
|
||||
end
|
||||
|
||||
dir_name =
|
||||
case dir
|
||||
when VSSpatialPos_T then '上'
|
||||
when VSSpatialPos_B then '下'
|
||||
when VSSpatialPos_L then '左'
|
||||
when VSSpatialPos_R then '右'
|
||||
when VSSpatialPos_F then '前'
|
||||
when VSSpatialPos_K then '后'
|
||||
end
|
||||
|
||||
input = UI.inputbox(["#{dir_name}分割(mm)"], [""], '区域分割')
|
||||
return unless input
|
||||
|
||||
len = input[0].to_i.mm
|
||||
if len <= 0
|
||||
UI.messagebox("输入数值小于等于0!")
|
||||
return
|
||||
end
|
||||
|
||||
params = {}
|
||||
params.store("method", SUZoneDiv1)
|
||||
params.store("uid", selected_zone.get_attribute("sw","uid"))
|
||||
params.store("zid", selected_zone.get_attribute("sw","zid"))
|
||||
params.store("dir", dir)
|
||||
params.store("len", len.to_mm)
|
||||
set_cmd("r00", params)
|
||||
end
|
||||
|
||||
def onLButtonDown(flags, x, y, view)
|
||||
helper = view.pick_helper
|
||||
helper.do_pick(x, y)
|
||||
picked = helper.best_picked
|
||||
if picked.class == Sketchup::Group && picked.valid? && picked.get_attribute("sw", "uid", nil)
|
||||
uid = picked.get_attribute("sw", "uid")
|
||||
zid = picked.get_attribute("sw", "zid")
|
||||
typ = picked.get_attribute("sw", "typ")
|
||||
if typ == "zid" && SUWimpl.selected_zone != picked
|
||||
data = {}
|
||||
data.store("uid", uid)
|
||||
data.store("zid", zid)
|
||||
data.store("pid", -1)
|
||||
data.store("cp", -1)
|
||||
set_cmd("r01", data)#select_client
|
||||
SUWimpl.instance.sel_zone_local(data)
|
||||
end
|
||||
end
|
||||
Sketchup.active_model.selection.clear
|
||||
end
|
||||
|
||||
def onKeyDown(key, rpt, flags, view)
|
||||
if key == VK_CONTROL#切换分割模式
|
||||
@pattern = @pattern == :up ? :back : :up
|
||||
reset_status_text
|
||||
end
|
||||
end
|
||||
|
||||
def onKeyUp(key, rpt, flags, view)
|
||||
case key
|
||||
when VK_UP
|
||||
@pattern == :back ? divide(VSSpatialPos_K) : divide(VSSpatialPos_T)
|
||||
when VK_DOWN
|
||||
@pattern == :back ? divide(VSSpatialPos_F) : divide(VSSpatialPos_B)
|
||||
when VK_LEFT then divide(VSSpatialPos_L) if @pattern == :up
|
||||
when VK_RIGHT then divide(VSSpatialPos_R) if @pattern == :up
|
||||
end
|
||||
end
|
||||
|
||||
def draw(view)
|
||||
Sketchup::set_status_text(@tooltip)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="25px" height="24px" viewBox="0 0 25 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.3.3 (12072) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>cursor_move</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1-Icons-24x24" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="cursor_move" sketch:type="MSArtboardGroup" transform="translate(1.000000, 0.000000)">
|
||||
<g id="MoveIcon" sketch:type="MSLayerGroup" transform="translate(3.000000, 4.000000)">
|
||||
<g id="Shadow" transform="translate(0.000000, 1.000000)" sketch:type="MSShapeGroup">
|
||||
<path d="M6.984375,12.8384726 L11.109375,12.390625 L9.34251538,15.390625 L6.984375,12.8384726 L6.984375,12.8384726 Z" id="Fill-24910" fill-opacity="0.1216" fill="#000000"></path>
|
||||
<path d="M6.984375,12.8385091 L11.109375,12.390625 L9.34251538,15.390625 L6.984375,12.8385091 L6.984375,12.8385091 Z" id="Stroke-24911" stroke-opacity="0.1255" stroke="#000000" stroke-width="1.875" stroke-linecap="round"></path>
|
||||
<path d="M13.5729735,9.25 L13.125,5.125 L16.125,6.89178834 L13.5729735,9.25 L13.5729735,9.25 Z" id="Fill-24914" fill-opacity="0.1216" fill="#000000"></path>
|
||||
<path d="M13.5729735,9.25 L13.125,5.125 L16.125,6.89175806 L13.5729735,9.25 L13.5729735,9.25 Z" id="Stroke-24915" stroke-opacity="0.1255" stroke="#000000" stroke-width="1.875" stroke-linecap="round"></path>
|
||||
<path d="M9.984375,2.6615086 L5.859375,3.109375 L7.62603864,0.109375 L9.984375,2.6615086 L9.984375,2.6615086 Z" id="Fill-24918" fill-opacity="0.1216" fill="#000000"></path>
|
||||
<path d="M9.984375,2.66148023 L5.859375,3.109375 L7.62603864,0.109375 L9.984375,2.66148023 L9.984375,2.66148023 Z" id="Stroke-24919" stroke-opacity="0.1255" stroke="#000000" stroke-width="1.875" stroke-linecap="round"></path>
|
||||
<path d="M3.5365086,6.109375 L3.984375,10.234375 L0.984375,8.46756879 L3.5365086,6.109375 L3.5365086,6.109375 Z" id="Fill-24922" fill-opacity="0.1216" fill="#000000"></path>
|
||||
<path d="M3.5365086,6.109375 L3.984375,10.234375 L0.984375,8.46759196 L3.5365086,6.109375 L3.5365086,6.109375 Z" id="Stroke-24923" stroke-opacity="0.1255" stroke="#000000" stroke-width="1.875" stroke-linecap="round"></path>
|
||||
</g>
|
||||
<g id="BlackLines" transform="translate(4.000000, 3.000000)" stroke="#000000" stroke-opacity="0.9882" stroke-width="1.875" sketch:type="MSShapeGroup">
|
||||
<path d="M3,5 L0.341064453,5" id="Stroke-24940"></path>
|
||||
<path d="M5,3 L5,0.437866211" id="Stroke-24947"></path>
|
||||
<path d="M9.65893555,5 L7,5" id="Stroke-24940" transform="translate(8.329468, 5.000000) rotate(-180.000000) translate(-8.329468, -5.000000) "></path>
|
||||
<path d="M5,9.56213379 L5,7" id="Stroke-24947" transform="translate(5.000000, 8.281067) rotate(-180.000000) translate(-5.000000, -8.281067) "></path>
|
||||
</g>
|
||||
<path d="M9,0 L11.5,4 L10,4 L10,6 L8,6 L8,4 L6.5,4 L9,0 Z" id="Path-7969" stroke="#FFFFFF" stroke-width="0.5" fill="#000000" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M4,5 L6.5,9 L5,9 L5,11 L3,11 L3,9 L1.5,9 L4,5 Z" id="Path-7969-Copy" stroke="#FFFFFF" stroke-width="0.5" fill="#000000" sketch:type="MSShapeGroup" transform="translate(4.000000, 8.000000) rotate(-90.000000) translate(-4.000000, -8.000000) "></path>
|
||||
<path d="M9,10 L11.5,14 L10,14 L10,16 L8,16 L8,14 L6.5,14 L9,10 Z" id="Path-7969-Copy-2" stroke="#FFFFFF" stroke-width="0.5" fill="#000000" sketch:type="MSShapeGroup" transform="translate(9.000000, 13.000000) rotate(-180.000000) translate(-9.000000, -13.000000) "></path>
|
||||
<path d="M14,5 L16.5,9 L15,9 L15,11 L13,11 L13,9 L11.5,9 L14,5 Z" id="Path-7969-Copy-3" stroke="#FFFFFF" stroke-width="0.5" fill="#000000" sketch:type="MSShapeGroup" transform="translate(14.000000, 8.000000) rotate(90.000000) translate(-14.000000, -8.000000) "></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 398 B |
After Width: | Height: | Size: 967 B |
After Width: | Height: | Size: 918 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
|
@ -2,8 +2,15 @@ import socket
|
|||
import json
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# 设置标准输出编码为 UTF-8(解决 Windows 控制台编码问题)
|
||||
if sys.platform.startswith('win'):
|
||||
import codecs
|
||||
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.detach())
|
||||
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.detach())
|
||||
|
||||
class JSONSocketServer:
|
||||
def __init__(self, host='localhost', port=8888):
|
||||
self.host = host
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Socket JSON 服务器 - 编码安全版本
|
||||
解决 Windows 控制台编码问题
|
||||
"""
|
||||
|
||||
import socket
|
||||
import json
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# 导入编码修复模块
|
||||
try:
|
||||
from encoding_fix import safe_print, safe_format_message
|
||||
except ImportError:
|
||||
# 如果导入失败,使用简单的备用函数
|
||||
def safe_print(text, fallback=None):
|
||||
try:
|
||||
print(text)
|
||||
except UnicodeEncodeError:
|
||||
print(text.encode('ascii', errors='ignore').decode('ascii'))
|
||||
|
||||
def safe_format_message(text):
|
||||
# 简单替换emoji为文本
|
||||
replacements = {
|
||||
"🚀": "[START]", "📍": "[ADDR]", "⏰": "[TIME]", "✅": "[OK]",
|
||||
"🔗": "[CONN]", "📨": "[MSG]", "❌": "[ERROR]", "🔌": "[DISC]",
|
||||
"📋": "[LIST]", "🖥️": "[SERVER]", "💻": "[CLIENT]", "📁": "[FILE]",
|
||||
"🎯": "[RESULT]", "⚠️": "[WARN]", "📊": "[REPORT]", "⛔": "[STOP]"
|
||||
}
|
||||
for emoji, replacement in replacements.items():
|
||||
text = text.replace(emoji, replacement)
|
||||
return text
|
||||
|
||||
class JSONSocketServerSafe:
|
||||
def __init__(self, host='localhost', port=8888):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.socket = None
|
||||
self.clients = []
|
||||
|
||||
def start_server(self):
|
||||
"""启动服务器"""
|
||||
try:
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.socket.bind((self.host, self.port))
|
||||
self.socket.listen(5)
|
||||
|
||||
safe_print("🚀 服务器启动成功!", "[START] 服务器启动成功!")
|
||||
safe_print(f"📍 监听地址: {self.host}:{self.port}", f"[ADDR] 监听地址: {self.host}:{self.port}")
|
||||
safe_print(f"⏰ 启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
f"[TIME] 启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
safe_print("✅ 等待客户端连接...", "[OK] 等待客户端连接...")
|
||||
|
||||
while True:
|
||||
client_socket, address = self.socket.accept()
|
||||
safe_print(f"🔗 客户端连接: {address}", f"[CONN] 客户端连接: {address}")
|
||||
|
||||
# 为每个客户端创建独立线程
|
||||
client_thread = threading.Thread(
|
||||
target=self.handle_client,
|
||||
args=(client_socket, address)
|
||||
)
|
||||
client_thread.daemon = True
|
||||
client_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
safe_print(f"❌ 服务器启动失败: {e}", f"[ERROR] 服务器启动失败: {e}")
|
||||
finally:
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
|
||||
def handle_client(self, client_socket, address):
|
||||
"""处理客户端请求"""
|
||||
try:
|
||||
while True:
|
||||
# 接收客户端消息
|
||||
data = client_socket.recv(4096).decode('utf-8')
|
||||
if not data:
|
||||
break
|
||||
|
||||
safe_print(f"📨 收到来自 {address} 的消息: {data}",
|
||||
f"[MSG] 收到来自 {address} 的消息: {data}")
|
||||
|
||||
# 解析客户端请求
|
||||
try:
|
||||
request = json.loads(data)
|
||||
response = self.process_request(request, address)
|
||||
|
||||
# 发送响应
|
||||
response_json = json.dumps(response, ensure_ascii=False, indent=2)
|
||||
client_socket.send(response_json.encode('utf-8'))
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是 JSON 格式,当作普通消息处理
|
||||
response = {
|
||||
"status": "success",
|
||||
"message": f"服务器收到消息: {data}",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"client_address": f"{address[0]}:{address[1]}"
|
||||
}
|
||||
response_json = json.dumps(response, ensure_ascii=False, indent=2)
|
||||
client_socket.send(response_json.encode('utf-8'))
|
||||
|
||||
except Exception as e:
|
||||
safe_print(f"❌ 处理客户端 {address} 时出错: {e}",
|
||||
f"[ERROR] 处理客户端 {address} 时出错: {e}")
|
||||
finally:
|
||||
client_socket.close()
|
||||
safe_print(f"🔌 客户端 {address} 已断开连接",
|
||||
f"[DISC] 客户端 {address} 已断开连接")
|
||||
|
||||
def process_request(self, request, address):
|
||||
"""处理客户端的 JSON 请求"""
|
||||
command = request.get('command', 'unknown')
|
||||
|
||||
if command == 'get_file':
|
||||
# 发送文件内容
|
||||
filename = request.get('filename', 'test_data.json')
|
||||
return self.send_file(filename, address)
|
||||
|
||||
elif command == 'save_file':
|
||||
# 保存文件
|
||||
filename = request.get('filename', 'received_data.json')
|
||||
content = request.get('content', {})
|
||||
return self.save_file(filename, content, address)
|
||||
|
||||
elif command == 'list_files':
|
||||
# 列出当前目录的 JSON 文件
|
||||
json_files = [f for f in os.listdir('.') if f.endswith('.json')]
|
||||
return {
|
||||
"status": "success",
|
||||
"command": "list_files",
|
||||
"files": json_files,
|
||||
"count": len(json_files),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"client_address": f"{address[0]}:{address[1]}"
|
||||
}
|
||||
|
||||
elif command == 'ping':
|
||||
# 心跳测试
|
||||
return {
|
||||
"status": "success",
|
||||
"command": "ping",
|
||||
"message": "pong",
|
||||
"server_time": datetime.now().isoformat(),
|
||||
"client_address": f"{address[0]}:{address[1]}"
|
||||
}
|
||||
|
||||
else:
|
||||
# 未知命令
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"未知命令: {command}",
|
||||
"available_commands": ["get_file", "save_file", "list_files", "ping"],
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"client_address": f"{address[0]}:{address[1]}"
|
||||
}
|
||||
|
||||
def send_file(self, filename, address):
|
||||
"""发送文件内容"""
|
||||
try:
|
||||
if os.path.exists(filename):
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
content = json.load(f)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"command": "get_file",
|
||||
"filename": filename,
|
||||
"content": content,
|
||||
"size": os.path.getsize(filename),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"client_address": f"{address[0]}:{address[1]}"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "error",
|
||||
"command": "get_file",
|
||||
"message": f"文件 {filename} 不存在",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"client_address": f"{address[0]}:{address[1]}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"command": "get_file",
|
||||
"message": f"读取文件失败: {str(e)}",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"client_address": f"{address[0]}:{address[1]}"
|
||||
}
|
||||
|
||||
def save_file(self, filename, content, address):
|
||||
"""保存文件"""
|
||||
try:
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(content, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"command": "save_file",
|
||||
"filename": filename,
|
||||
"message": f"文件 {filename} 保存成功",
|
||||
"size": os.path.getsize(filename),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"client_address": f"{address[0]}:{address[1]}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"command": "save_file",
|
||||
"message": f"保存文件失败: {str(e)}",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"client_address": f"{address[0]}:{address[1]}"
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = JSONSocketServerSafe()
|
||||
try:
|
||||
server.start_server()
|
||||
except KeyboardInterrupt:
|
||||
safe_print("\n⛔ 服务器已停止", "\n[STOP] 服务器已停止")
|
1
test.bat
|
@ -1,5 +1,6 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
set PYTHONIOENCODING=utf-8
|
||||
echo.
|
||||
echo ===============================================
|
||||
echo Socket JSON 传输系统 - Windows 测试工具
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"test_time": "2025-07-01T14:03:13.108361",
|
||||
"test_results": [
|
||||
"[14:03:11] INFO: 🚀 开始 Socket JSON 传输系统完整测试",
|
||||
"[14:03:11] INFO: ==================================================",
|
||||
"[14:03:11] INFO: \n📋 测试 1/4: 验证文件存在性",
|
||||
"[14:03:11] INFO: 验证测试文件...",
|
||||
"[14:03:11] SUCCESS: ✅ 所有必需文件都存在",
|
||||
"[14:03:11] INFO: \n🖥️ 测试 2/4: 启动服务器",
|
||||
"[14:03:11] INFO: 正在启动服务器...",
|
||||
"[14:03:13] ERROR: ❌ 服务器启动失败: Traceback (most recent call last):\n File \"C:\\Users\\20920\\Desktop\\blender\\server.py\", line 22, in start_server\n print(f\"\\U0001f680 服务器启动成功!\")\n ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^\nUnicodeEncodeError: 'gbk' codec can't encode character '\\U0001f680' in position 0: illegal multibyte sequence\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n File \"C:\\Users\\20920\\Desktop\\blender\\server.py\", line 189, in <module>\n server.start_server()\n ~~~~~~~~~~~~~~~~~~~^^\n File \"C:\\Users\\20920\\Desktop\\blender\\server.py\", line 40, in start_server\n print(f\"\\u274c 服务器启动失败: {e}\")\n ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^\nUnicodeEncodeError: 'gbk' codec can't encode character '\\u274c' in position 0: illegal multibyte sequence\n",
|
||||
"[14:03:13] INFO: ⛔ 服务器已停止",
|
||||
"[14:03:13] INFO: \n==================================================",
|
||||
"[14:03:13] WARNING: 🎯 测试完成: 1/4 项通过",
|
||||
"[14:03:13] WARNING: ⚠️ 部分测试失败,请检查上述错误信息"
|
||||
],
|
||||
"summary": "测试完成,详细日志共 12 条"
|
||||
}
|
|
@ -13,6 +13,12 @@ import sys
|
|||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 设置标准输出编码为 UTF-8(解决 Windows 控制台编码问题)
|
||||
if sys.platform.startswith('win'):
|
||||
import codecs
|
||||
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.detach())
|
||||
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.detach())
|
||||
|
||||
class SocketTester:
|
||||
def __init__(self):
|
||||
self.server_process = None
|
||||
|
|
28
使用说明.md
|
@ -256,6 +256,34 @@ server = JSONSocketServer(host='0.0.0.0', port=9999)
|
|||
client = JSONSocketClient(host='192.168.1.100', port=9999)
|
||||
```
|
||||
|
||||
### 解决 Windows 编码问题
|
||||
|
||||
如果在 Windows 控制台遇到 Unicode 编码错误:
|
||||
|
||||
1. **使用编码安全版本**:
|
||||
```bash
|
||||
# 使用编码安全的服务器
|
||||
python server_safe.py
|
||||
```
|
||||
|
||||
2. **设置控制台编码**:
|
||||
```cmd
|
||||
# 在 CMD 中运行
|
||||
chcp 65001
|
||||
set PYTHONIOENCODING=utf-8
|
||||
python server.py
|
||||
```
|
||||
|
||||
3. **使用现代终端**:
|
||||
- Windows Terminal(推荐)
|
||||
- VS Code 集成终端
|
||||
- PowerShell 7
|
||||
|
||||
4. **文件包含**:
|
||||
- `encoding_fix.py` - 编码修复模块
|
||||
- `server_safe.py` - 编码安全的服务器版本
|
||||
- `requirements.txt` - 依赖说明(无需额外包)
|
||||
|
||||
## 🛡️ 安全注意事项
|
||||
|
||||
- 此系统仅用于学习和测试,不适用于生产环境
|
||||
|
|