修复Windows编码问题 - 添加编码安全版本和requirements.txt

This commit is contained in:
Pei Xueke 2025-07-01 14:06:42 +08:00
parent 50e196ad9b
commit bf81d6a23d
30 changed files with 3593 additions and 0 deletions

View File

@ -1,8 +1,17 @@
import socket import socket
import json import json
import time import time
import sys
from datetime import datetime 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: class JSONSocketClient:
def __init__(self, host='localhost', port=8888): def __init__(self, host='localhost', port=8888):
self.host = host self.host = host

111
encoding_fix.py Normal file
View File

@ -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()

26
requirements.txt Normal file
View File

@ -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 编码

BIN
ruby.rar Normal file

Binary file not shown.

118
ruby/ruby/SUWClient.rb Normal file
View File

@ -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
}

305
ruby/ruby/SUWConstants.rb Normal file
View File

@ -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

2019
ruby/ruby/SUWImpl.rb Normal file

File diff suppressed because it is too large Load Diff

12
ruby/ruby/SUWLoad.rb Normal file
View File

@ -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')

71
ruby/ruby/SUWMenu.rb Normal file
View File

@ -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

87
ruby/ruby/SUWObserver.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -2,8 +2,15 @@ import socket
import json import json
import threading import threading
import os import os
import sys
from datetime import datetime 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: class JSONSocketServer:
def __init__(self, host='localhost', port=8888): def __init__(self, host='localhost', port=8888):
self.host = host self.host = host

226
server_safe.py Normal file
View File

@ -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] 服务器已停止")

View File

@ -1,5 +1,6 @@
@echo off @echo off
chcp 65001 >nul chcp 65001 >nul
set PYTHONIOENCODING=utf-8
echo. echo.
echo =============================================== echo ===============================================
echo Socket JSON 传输系统 - Windows 测试工具 echo Socket JSON 传输系统 - Windows 测试工具

18
test_report.json Normal file
View File

@ -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 条"
}

View File

@ -13,6 +13,12 @@ import sys
import json import json
from datetime import datetime 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: class SocketTester:
def __init__(self): def __init__(self):
self.server_process = None self.server_process = None

View File

@ -256,6 +256,34 @@ server = JSONSocketServer(host='0.0.0.0', port=9999)
client = JSONSocketClient(host='192.168.1.100', 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` - 依赖说明(无需额外包)
## 🛡️ 安全注意事项 ## 🛡️ 安全注意事项
- 此系统仅用于学习和测试,不适用于生产环境 - 此系统仅用于学习和测试,不适用于生产环境