From bf81d6a23d51785516059f2c63c34976a9acb377 Mon Sep 17 00:00:00 2001 From: Pei Xueke Date: Tue, 1 Jul 2025 14:06:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DWindows=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20-=20=E6=B7=BB=E5=8A=A0=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E7=89=88=E6=9C=AC=E5=92=8Crequirements.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.py | 9 + encoding_fix.py | 111 ++ requirements.txt | 26 + ruby.rar | Bin 0 -> 36141 bytes ruby/ruby/SUWClient.rb | 118 ++ ruby/ruby/SUWConstants.rb | 305 +++++ ruby/ruby/SUWImpl.rb | 2019 +++++++++++++++++++++++++++++++ ruby/ruby/SUWLoad.rb | 12 + ruby/ruby/SUWMenu.rb | 71 ++ ruby/ruby/SUWObserver.rb | 87 ++ ruby/ruby/SUWUnitContTool.rb | 137 +++ ruby/ruby/SUWUnitFaceTool.rb | 145 +++ ruby/ruby/SUWUnitPointTool.rb | 128 ++ ruby/ruby/SUWZoneDiv1Tool.rb | 106 ++ ruby/ruby/icon/cursor_move.svg | 33 + ruby/ruby/icon/order_quote.png | Bin 0 -> 398 bytes ruby/ruby/icon/unit_delete.png | Bin 0 -> 967 bytes ruby/ruby/icon/unit_delete1.png | Bin 0 -> 918 bytes ruby/ruby/icon/unit_face.png | Bin 0 -> 1100 bytes ruby/ruby/icon/unit_face1.png | Bin 0 -> 1034 bytes ruby/ruby/icon/unit_point.png | Bin 0 -> 1039 bytes ruby/ruby/icon/unit_point1.png | Bin 0 -> 1031 bytes ruby/ruby/icon/zone_div1.png | Bin 0 -> 1121 bytes ruby/ruby/icon/zone_div11.png | Bin 0 -> 1137 bytes server.py | 7 + server_safe.py | 226 ++++ test.bat | 1 + test_report.json | 18 + test_socket.py | 6 + 使用说明.md | 28 + 30 files changed, 3593 insertions(+) create mode 100644 encoding_fix.py create mode 100644 requirements.txt create mode 100644 ruby.rar create mode 100644 ruby/ruby/SUWClient.rb create mode 100644 ruby/ruby/SUWConstants.rb create mode 100644 ruby/ruby/SUWImpl.rb create mode 100644 ruby/ruby/SUWLoad.rb create mode 100644 ruby/ruby/SUWMenu.rb create mode 100644 ruby/ruby/SUWObserver.rb create mode 100644 ruby/ruby/SUWUnitContTool.rb create mode 100644 ruby/ruby/SUWUnitFaceTool.rb create mode 100644 ruby/ruby/SUWUnitPointTool.rb create mode 100644 ruby/ruby/SUWZoneDiv1Tool.rb create mode 100644 ruby/ruby/icon/cursor_move.svg create mode 100644 ruby/ruby/icon/order_quote.png create mode 100644 ruby/ruby/icon/unit_delete.png create mode 100644 ruby/ruby/icon/unit_delete1.png create mode 100644 ruby/ruby/icon/unit_face.png create mode 100644 ruby/ruby/icon/unit_face1.png create mode 100644 ruby/ruby/icon/unit_point.png create mode 100644 ruby/ruby/icon/unit_point1.png create mode 100644 ruby/ruby/icon/zone_div1.png create mode 100644 ruby/ruby/icon/zone_div11.png create mode 100644 server_safe.py create mode 100644 test_report.json diff --git a/client.py b/client.py index f622e78..13697fc 100644 --- a/client.py +++ b/client.py @@ -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 diff --git a/encoding_fix.py b/encoding_fix.py new file mode 100644 index 0000000..c5e686f --- /dev/null +++ b/encoding_fix.py @@ -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() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..66aef72 --- /dev/null +++ b/requirements.txt @@ -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 编码 \ No newline at end of file diff --git a/ruby.rar b/ruby.rar new file mode 100644 index 0000000000000000000000000000000000000000..60d1c9c4eb3040c904262f3fcf371e16ae23ed7f GIT binary patch literal 36141 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 Y01yBG0CII=c>wVC`+&qfdjLQO0MZ-FLjV8( literal 0 HcmV?d00001 diff --git a/ruby/ruby/SUWClient.rb b/ruby/ruby/SUWClient.rb new file mode 100644 index 0000000..f9f2398 --- /dev/null +++ b/ruby/ruby/SUWClient.rb @@ -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 +} \ No newline at end of file diff --git a/ruby/ruby/SUWConstants.rb b/ruby/ruby/SUWConstants.rb new file mode 100644 index 0000000..4acc18c --- /dev/null +++ b/ruby/ruby/SUWConstants.rb @@ -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 diff --git a/ruby/ruby/SUWImpl.rb b/ruby/ruby/SUWImpl.rb new file mode 100644 index 0000000..f0c3b72 --- /dev/null +++ b/ruby/ruby/SUWImpl.rb @@ -0,0 +1,2019 @@ +require "singleton" + +module Geom + class Point3d + def self.parse(value) + if value.nil? || value.strip == "" then nil + else + xyz = value.gsub(/\(*\)*/, "").split(",").collect{|axis| axis.strip.to_f} + Point3d.new(xyz[0].mm, xyz[1].mm, xyz[2].mm) + end + end + + def to_s(unit="mm", digits = -1) + if unit=="cm" + "(#{x.to_f.to_cm.round(3)}, #{y.to_f.to_cm.round(3)}, #{z.to_f.to_cm.round(3)})" + elsif digits == -1 + "(#{x.to_f.to_mm}, #{y.to_f.to_mm}, #{z.to_f.to_mm})" + else + "(#{x.to_f.to_mm.round(digits)}, #{y.to_f.to_mm.round(digits)}, #{z.to_f.to_mm.round(digits)})" + end + end + # def to_s(); "(#{x.to_f.decimal(1)}, #{y.to_f.decimal(1)}, #{z.to_f.decimal(1)})" end + end + + class Vector3d + def self.parse(value) + if value.nil? || value.strip == "" then nil + else + xyz = value.gsub(/\(*\)*/, "").split(",").collect{|axis| axis.strip.to_f} + Vector3d.new(xyz[0].mm, xyz[1].mm, xyz[2].mm) + end + end + def to_s(unit="mm") + if unit=="cm" + "(#{x.to_f.to_cm.round(3)}, #{y.to_f.to_cm.round(3)}, #{z.to_f.to_cm.round(3)})" + elsif unit =="in" + "(#{x.to_f}, #{y.to_f}, #{z.to_f})" + else + "(#{x.to_f.to_mm}, #{y.to_f.to_mm}, #{z.to_f.to_mm})" + end + end + end + + class Transformation + def self.parse(data) + o = Geom::Point3d.parse(data.fetch("o")) + x = Geom::Vector3d.parse(data.fetch("x")) + y = Geom::Vector3d.parse(data.fetch("y")) + z = Geom::Vector3d.parse(data.fetch("z")) + Geom::Transformation.new(x, y, z, o) + end + + def store(data) + data.store("o", origin.to_s("mm")) + data.store("x", xaxis.to_s("in")) + data.store("y", yaxis.to_s("in")) + data.store("z", zaxis.to_s("in")) + end + end +end + +module Sketchup + class Face + def to_json(trans_arr = nil, digits = -1, arced = true) + edges = outer_loop.edges.collect{|edge| + edge.curve.is_a?(Sketchup::ArcCurve) && arced ? edge.curve : edge + }.uniq + + if edges.empty? + {}#返回值 + else + zaxis = normal + segs = edges.collect{|object| + seg = Hash.new + p1 = object.is_a?(Sketchup::Edge) ? object.start.position : object.first_edge.start.position + p2 = object.is_a?(Sketchup::Edge) ? object.end.position : object.last_edge.end.position + c = object.is_a?(Sketchup::Edge) ? nil : object.center + if trans_arr + trans_arr.each{|trans| + p1.transform!(trans) + p2.transform!(trans) + c.transform!(trans) if c + } + end + seg.store("s", p1.to_s("mm", digits)) + seg.store("e", p2.to_s("mm", digits)) + if c + seg.store("c", c.to_s("mm", digits)) + seg.store("a1", object.start_angle.to_f) + seg.store("a2", object.end_angle.to_f) + end + seg + } + + if trans_arr + trans_arr.each{|trans| + zaxis.transform!(trans) + } + end + {:zaxis => zaxis.normalize.to_s("in"), :segs => segs}#返回值 + end + end + end +end + +module SUWood + MAT_TYPE_NORMAL = 0 unless defined?(MAT_TYPE_NORMAL) + MAT_TYPE_OBVERSE = 1 unless defined?(MAT_TYPE_OBVERSE) + MAT_TYPE_NATURE = 2 unless defined?(MAT_TYPE_NATURE) + + class SUWimpl + include Singleton + + @@selected_uid = nil + @@selected_obj = nil#zid/pid/cp + @@selected_zone = nil + @@selected_part = nil + @@scaled_zone = nil + @@server_path = nil + attr_accessor :added_contour + + def startup() + @door_layer = Sketchup.active_model.layers.add("DOOR_LAYER") #门板开向线的图层 + @drawer_layer = Sketchup.active_model.layers.add("DRAWER_LAYER") + @door_layer.visible = true + @drawer_layer.visible = true + + @added_contour = false + + @textures = {} + add_mat_rgb("mat_normal", 0.1, 128, 128, 128)#Gray + add_mat_rgb("mat_select", 0.5, 255, 0, 0) #Red + add_mat_rgb("mat_default", 0.9, 255, 250, 250) + add_mat_rgb("mat_obverse", 1.0, 3, 70, 24) #Green + add_mat_rgb("mat_reverse", 1.0, 249, 247, 174)#Yellow + add_mat_rgb("mat_thin", 1.0, 248, 137, 239)#Pink purple + add_mat_rgb("mat_machine", 1.0, 0, 0, 255)#Blue + + @unit_param = {}#key uid, params such as w/d/h/order_id + @unit_trans = {}#key uid, trans of unit + + @zones = {}#key uid/oid + @parts = {}#key uid/cp, second key is component root oid + @hardwares = {}#key uid/cp, second key is hardware root oid + @machinings = {}#key uid, array, child entity of part or hardware + @dimensions = {}#key uid, array + @labels = Sketchup.active_model.entities.add_group + @door_labels= Sketchup.active_model.entities.add_group + + @part_mode = false + @hide_none = false + @mat_type = MAT_TYPE_NORMAL + + @selected_faces = [] + @selected_parts = [] + @selected_hws = [] + @menu_handle = 0 + @back_material = false + + default_surfs = [{"f"=>1, "p"=>1, "segs"=>[["(0,0,1000)", "(0,0,0)"], ["(0,0,0)", "(1000,0,0)"], ["(1000,0,0)", "(1000,0,1000)"], ["(1000,0,1000)", "(0,0,1000)"]], "vx"=>"(0,0,-1)", "vz"=>"(0,-1,0)"}, + {"f"=>4, "p"=>4, "segs"=>[["(1000,0,1000)", "(1000,0,0)"], ["(1000,0,0)", "(1000,1000,0)"], ["(1000,1000,0)", "(1000,1000,1000)"], ["(1000,1000,1000)", "(1000,0,1000)"]], "vx"=>"(0,0,-1)", "vz"=>"(1,0,0)"}, + {"f"=>2, "p"=>2, "segs"=>[["(0,1000,1000)", "(0,1000,0)"], ["(0,1000,0)", "(1000,1000,0)"], ["(1000,1000,0)", "(1000,1000,1000)"], ["(1000,1000,1000)", "(0,1000,1000)"]], "vx"=>"(0,0,-1)", "vz"=>"(0,-1,0)"}, + {"f"=>3, "p"=>3, "segs"=>[["(0,0,1000)", "(0,0,0)"], ["(0,0,0)", "(0,1000,0)"], ["(0,1000,0)", "(0,1000,1000)"], ["(0,1000,1000)", "(0,0,1000)"]], "vx"=>"(0,0,-1)", "vz"=>"(1,0,0)"}, + {"f"=>5, "p"=>5, "segs"=>[["(0,0,0)", "(1000,0,0)"], ["(1000,0,0)", "(1000,1000,0)"], ["(1000,1000,0)", "(0,1000,0)"], ["(0,1000,0)", "(0,0,0)"]], "vx"=>"(1,0,0)", "vz"=>"(0,0,1)"}, + {"f"=>6, "p"=>6, "segs"=>[["(0,0,1000)", "(1000,0,1000)"], ["(1000,0,1000)", "(1000,1000,1000)"], ["(1000,1000,1000)", "(0,1000,1000)"], ["(0,1000,1000)", "(0,0,1000)"]], "vx"=>"(1,0,0)", "vz"=>"(0,0,1)"}] + @@default_zone = Sketchup.active_model.entities.add_group + default_surfs.each{|surf| + face = create_face(@@default_zone, surf) + face.set_attribute("sw", "p", surf.fetch("p")) + } + @@default_zone.visible = false + + data = {"children"=>[{"child"=>7468, "surf"=>{"f"=>1, "p"=>1, "segs"=>[["(600,0,2400)", "(600,0,50)"], ["(600,0,50)", "(600,1932,50)"], ["(600,1932,50)", "(600,1932,2400)"], ["(600,1932,2400)", "(600,0,2400)"]], "vx"=>"(0,0,-1)", "vz"=>"(1,0,0)"}}, + {"child"=>7469, "surf"=>{"f"=>4, "p"=>4, "segs"=>[["(600,1932,2400)", "(600,1932,50)"], ["(600,1932,50)", "(0,1932,50)"], ["(0,1932,50)", "(0,1932,2400)"], ["(0,1932,2400)", "(600,1932,2400)"]], "vx"=>"(0,0,-1)", "vz"=>"(0,1,0)"}}, + {"child"=>7470, "surf"=>{"f"=>2, "p"=>2, "segs"=>[["(0,0,2400)", "(0,0,50)"], ["(0,0,50)", "(0,1932,50)"], ["(0,1932,50)", "(0,1932,2400)"], ["(0,1932,2400)", "(0,0,2400)"]], "vx"=>"(0,0,-1)", "vz"=>"(1,0,0)"}}, + {"child"=>7471, "surf"=>{"f"=>3, "p"=>3, "segs"=>[["(600,0,2400)", "(600,0,50)"], ["(600,0,50)", "(0,0,50)"], ["(0,0,50)", "(0,0,2400)"], ["(0,0,2400)", "(600,0,2400)"]], "vx"=>"(0,0,-1)", "vz"=>"(0,1,0)"}}, + {"child"=>7472, "surf"=>{"f"=>5, "p"=>5, "segs"=>[["(600,0,50)", "(600,1932,50)"], ["(600,1932,50)", "(0,1932,50)"], ["(0,1932,50)", "(0,0,50)"], ["(0,0,50)", "(600,0,50)"]], "vx"=>"(0,1,0)", "vz"=>"(0,0,1)"}}, + {"child"=>7473, "surf"=>{"f"=>6, "p"=>6, "segs"=>[["(600,0,2400)", "(600,1932,2400)"], ["(600,1932,2400)", "(0,1932,2400)"], ["(0,1932,2400)", "(0,0,2400)"], ["(0,0,2400)", "(600,0,2400)"]], "vx"=>"(0,1,0)", "vz"=>"(0,0,1)"}}], + "cmd"=>"c03", "depth"=>600, "height"=>2350, + "t_point"=>{"o"=>"(0,0,0)", "x"=>"(1,0,0)", "y"=>"(0,1,0)", "z"=>"(0,0,1)"}, + "t_scale"=>{"o"=>"(0,0,0)", "x"=>"(1.932,0,0)", "y"=>"(0,0.6,0)", "z"=>"(0,0,2.35)"}, + "trans"=>{"o"=>"(600,0,50)", "x"=>"(0,1.932,0)", "y"=>"(-0.6,0,0)", "z"=>"(0,0,2.35)"}, + "uid"=>"426c2d06c000000e", + "v_trans"=>{"o"=>"(600,0,50)", "x"=>"(0,1,0)", "y"=>"(-1,0,0)", "z"=>"(0,0,1)"}, + "width"=>1932, "zid"=>6147, "zip"=>1166} + end + + def add_mat_rgb(id, alpha, r, g, b) + mat = Sketchup.active_model.materials.add + mat.color = Sketchup::Color.new(r, g, b) + mat.alpha = alpha + @textures.store(id, mat) + end + + def get_zones(data) + uid = data.fetch("uid") + unless @zones.key?(uid) + @zones.store(uid, Hash.new) + end + @zones.fetch(uid) + end + + def get_parts(data) + uid = data.fetch("uid") + unless @parts.key?(uid) + @parts.store(uid, Hash.new) + end + @parts.fetch(uid) + end + + def get_hardwares(data) + uid = data.fetch("uid") + unless @hardwares.key?(uid) + @hardwares.store(uid, Hash.new) + end + @hardwares.fetch(uid) + end + + def c11(data) #part_obverse + @mat_type = data.fetch("v", false) ? MAT_TYPE_OBVERSE : MAT_TYPE_NORMAL + parts = get_parts(data) + parts.each{|root, part| + textured_part(part, false) unless part.nil? || !part.valid? || @selected_parts.include?(part) + } + end + + def c30(data) #part_nature + @mat_type = data.fetch("v", false) ? MAT_TYPE_NATURE : MAT_TYPE_NORMAL + parts = get_parts(data) + parts.each{|root, part| + textured_part(part, false) unless part.nil? || !part.valid? || @selected_parts.include?(part) + } + end + + def set_config(data) + if data.key?("server_path") + @@server_path = data.fetch("server_path") + end + if data.key?("order_id") + Sketchup.active_model.set_attribute("sw", "order_id", data.fetch("order_id")) + end + if data.key?("order_code") + Sketchup.active_model.set_attribute("sw", "order_code", data.fetch("order_code")) + end + if data.key?("back_material") + @back_material = data.fetch("back_material") + end + if data.key?("part_mode") + @part_mode = data.fetch("part_mode") + end + if data.key?("hide_none") + @hide_none = data.fetch("hide_none") + end + if data.key?("unit_drawing") + puts "#{data.fetch("drawing_name")}:\t#{data.fetch("unit_drawing")}" + end + if data.key?("zone_corner") + zones = get_zones(data) + zone = zones.fetch(data.fetch("zid"), nil) + if zone + zone.set_attribute("sw", "cor", data.fetch("zone_corner")) + end + end + end + + def c02(data) #add_texture + ckey = data.fetch("ckey") + if @textures.include?(ckey) + texture = @textures.fetch(ckey) + if texture.valid? + return + end + end + + material = Sketchup.active_model.materials.add + material.texture = data.fetch("src") + material.alpha = data.fetch("alpha") + texture = material.texture + unless texture.nil? + texture.size = [texture.image_width.mm, texture.image_height.mm] + @textures.store(ckey, material) + end + end + + def get_texture(key)#key maybe nil when edge etc. + if key && @textures.include?(key) + @textures.fetch(key) + else + @textures.fetch("mat_default") + end + end + + def c03(data) #add_zone + uid = data.fetch("uid") + zid = data.fetch("zid") + zones = get_zones(data) + elements = data.fetch("children") + if data.key?("trans") + poses = {} + elements.each{|element| + surf = element.fetch("surf") + poses.store(surf.fetch("p"), element.fetch("child")) + } + scale = Geom::Transformation.scaling(data.fetch("w").mm.to_f / 1000.0, + data.fetch("d").mm.to_f / 1000.0, + data.fetch("h").mm.to_f / 1000.0) + trans = Geom::Transformation::parse(data.fetch("t")) + group = @@default_zone.copy + group.transform!(scale)#单独缩放, 否则无效 + group.transform!(trans) + group.visible = true + group.entities.each{|entity| + if entity.class == Sketchup::Face + p = entity.get_attribute("sw", "p") + entity.set_attribute("sw", "child", poses.fetch(p)) + if p == 1 + entity.layer = @door_layer + end + end + } + else + group = Sketchup.active_model.entities.add_group + elements.each{|element| + surf = element.fetch("surf") + face = create_face(group, surf) + if surf.fetch("p") == 1 + face.layer = @door_layer + end + face.set_attribute("sw", "child", element.fetch("child")) + } + end + group.set_attribute("sw", "uid", uid) + group.set_attribute("sw", "zid", zid) + group.set_attribute("sw", "zip", data.fetch("zip", -1)) + group.set_attribute("sw", "typ", "zid") + group.set_attribute("sw", "cor", data.fetch("cor")) if data.key?("cor") + + if @unit_trans.key?(uid) + group.transform!(@unit_trans.fetch(uid)) + end + group.make_unique + group.definition.behavior.no_scale_mask = 127#限制所有方向的拉伸 + zones.store(zid, group) + end + + def c06(data) #add_wall + zones = get_zones(data) + zid = data.fetch("zid") + zone = zones.fetch(zid, nil) + elements = data.fetch("children") + elements.each{|element| + surf = element.fetch("surf") + face = create_face(zone, surf) + if surf.fetch("p") == 1 + face.layer = @door_layer + end + face.set_attribute("sw", "child", element.fetch("child")) + } + end + + def c04(data) #add_part + def add_part_profile(face, index, profiles) + profile = profiles.fetch(index, nil) + color = profile.nil? ? nil : profile.fetch("ckey", nil) + scale = profile.nil? ? nil : profile.fetch("scale", nil) + angle = profile.nil? ? nil : profile.fetch("angle", nil) + type = profile.nil? ? "0" : profile.fetch("typ", "0") + current = + if @mat_type == MAT_TYPE_OBVERSE + case type + when 1 then "mat_obverse" #thick profile + when 2 then "mat_thin" #thin profile + else "mat_reverse" #none profile + end + else + color + end + face.set_attribute("sw", "typ", "e#{type}") + textured_surf(face, @back_material, current, color, scale, angle) + end + + def add_part_board(part, data, antiz, profiles) + leaf = part.entities.add_group + color = data.fetch("ckey") + scale = data.fetch("scale", nil) + angle = data.fetch("angle", nil) + color2 = data.fetch("ckey2", nil) + scale2 = data.fetch("scale2", nil) + angle2 = data.fetch("angle2", nil) + + leaf.set_attribute("sw", "ckey", color) + leaf.set_attribute("sw", "scale", scale) if scale + leaf.set_attribute("sw", "angle", angle) if angle + if data.key?("sects") + sects = data.fetch("sects") + sects.each{|sect| + segs = sect.fetch("segs") + surf = sect.fetch("sect") + paths = create_paths(part, segs) + follow_me(leaf, surf, paths, color, scale, angle) + } + leaf2 = leaf.entities.add_group + add_part_surf(leaf2, data, antiz, color, scale, angle, color2, scale2, angle2, profiles) + else + add_part_surf(leaf, data, antiz, color, scale, angle, color2, scale2, angle2, profiles) + end + + leaf + end + + def add_part_surf(leaf, data, antiz, color, scale, angle, color2, scale2, angle2, profiles) + #will store attr when add face, also reverse the rev's normal + obv = data.fetch("obv") + rev = data.fetch("rev") + obv_type = "o" + obv_save = color + obv_scale = scale + obv_angle = angle + rev_type = "r" + rev_save = color2.nil? ? color : color2 + rev_scale = color2.nil? ? scale : scale2 + rev_angle = color2.nil? ? angle : angle2 + if antiz#交换type/save/scale/angle + obv_type, rev_type = rev_type, obv_type + obv_save, rev_save = rev_save, obv_save + obv_scale, rev_scale = rev_scale, obv_scale + obv_angle, rev_angle = rev_angle, obv_angle + end + obv_show = @mat_type == MAT_TYPE_OBVERSE ? "mat_obverse" : obv_save + rev_show = @mat_type == MAT_TYPE_OBVERSE ? "mat_reverse" : rev_save + + series1 = [] + series2 = [] + create_face(leaf, obv, obv_show, obv_scale, obv_angle, series1, false, @back_material, obv_save, obv_type)#obv + create_face(leaf, rev, rev_show, rev_scale, rev_angle, series2, true, @back_material, rev_save, rev_type)#rev + add_part_edges(leaf, series1, series2, obv, rev, profiles) + end + + def add_part_edges(leaf, series1, series2, obv, rev, profiles = nil) + unplanar = false + series1.each_index{|index| + pts1 = series1[index] + pts2 = series2[index] + pts1.each_index{|i| + next if i.zero? + + pts = [pts1[i-1], pts1[i], pts2[i], pts2[i-1]] + begin + face = leaf.entities.add_face(pts) + face.edges.each_index{|k| + face.edges[k].hidden = true if [1, 3].include?(k) + } + add_part_profile(face, index, profiles) unless profiles.nil? + rescue + unplanar = true + puts "Points are not planar #{index}: #{i}" + puts pts + end + } + } + + if unplanar + segs_o = obv.fetch("segs"); pts_o = []; segs_o.each{|seg| pts_o << seg[0]} + segs_r = rev.fetch("segs"); pts_r = []; segs_r.each{|seg| pts_r << seg[0]} + puts "===========================" + puts "obv:\t#{pts_o.join("\t")}" + puts "rev:\t#{pts_r.join("\t")}" + puts "series1:\t#{series1.join("\t")}" + puts "series2:\t#{series2.join("\t")}" + puts "===========================" + end + end + + def add_part_stretch(part, data) + copmensates = data.fetch("copmensates") + trim_surfs = data.fetch("trim_surfs") + baselines = create_paths(part, data.fetch("baselines")) + inst = nil + if data.include?("sid") && copmensates.empty? && trim_surfs.empty? && baselines.length == 1 + file = "#{SUWood::suwood_path(V_StretchPart)}/#{data.fetch("sid")}.skp" + if File.exist?(file) && File.size(file) > 0 + begin + defi = Sketchup.active_model.definitions.load(file) + inst = part.entities.add_instance(defi, ORIGIN) + inst.set_attribute("sw", "typ", "cp") + xrate = baselines[0].length.to_mm / inst.bounds.width.to_mm + trans = Geom::Transformation::parse(data) + inst.transform!(Geom::Transformation.scaling(xrate, 1, 1)) + inst.transform!(trans) + rescue => e + puts e + inst = nil + end + end + end + if inst + leaf = part.entities.add_group + surf = data.fetch("sect") + surf.store("segs", data.fetch("bounds")) + follow_me(leaf, surf, baselines, nil) + leaf.set_attribute("sw", "virtual", true) + leaf.visible = false + else + thick = data.fetch("thick").mm + leaf = part.entities.add_group + zaxis = Geom::Vector3d.parse(data.fetch("zaxis")) + color = data.fetch("ckey") + sect = data.fetch("sect") + follow_me(leaf, sect, baselines, color) + copmensates.each{|copmensate| + points = copmensate.collect{|point| Geom::Point3d.parse(point)} + path = part.entities.add_line(points[0].offset(zaxis, -1.mm), points[0].offset(zaxis, thick + 1.mm))#要稍微大点! + trimmer = part.entities.add_group + face_t = trimmer.entities.add_face(points) + face_t.followme(path) + + trimmed = trimmer.trim(leaf) + leaf = trimmed if trimmed + path.erase! if path && path.valid? + trimmer.erase! if trimmer && trimmer.valid? + } + trim_surfs.each{|trim_surf| + trimmer = part.entities.add_group + face_t = create_face(trimmer, trim_surf) + point = face_t.edges[0].start.position + path = part.entities.add_line(point.offset(zaxis, -1.mm), point.offset(zaxis, thick + 1.mm))#要稍微大点! + face_t.followme(path) + + trimmed = trimmer.trim(leaf) + leaf = trimmed if trimmed + path.erase! if path && path.valid? + trimmer.erase! if trimmer && trimmer.valid? + } + leaf.set_attribute("sw", "ckey", color) + end + leaf + end + + def add_part_arc(part, data, antiz, profiles) + leaf = part.entities.add_group + obv = data.fetch("obv") + color = data.fetch("ckey") + scale = data.fetch("scale", nil) + angle = data.fetch("angle", nil) + color2 = data.fetch("ckey2", nil) + scale2 = data.fetch("scale2", nil) + angle2 = data.fetch("angle2", nil) + + leaf.set_attribute("sw", "ckey", color) + leaf.set_attribute("sw", "scale", scale) if scale + leaf.set_attribute("sw", "angle", angle) if angle + + center_o = Geom::Point3d.parse(data.fetch("co")) + center_r = Geom::Point3d.parse(data.fetch("cr")) + path = leaf.entities.add_line(center_o, center_r) + series = [] + normal = follow_me(leaf, obv, path, color, scale, angle, false, series, true) + + if series.size == 4 + count = 0 + edge1 = false + edge3 = false + face2 = color2.nil? + leaf.entities.each{|entity| + next unless entity.class == Sketchup::Face + + if entity.normal.parallel?(normal) + if center_o.on_plane?(entity.plane) + add_part_profile(entity, 2, profiles) + count += 1 + else + add_part_profile(entity, 0, profiles) + count += 1 + end + else + entity.edges.each{|edge| + s = edge.start.position + e = edge.end.position + if !edge1 && series[1].include?(s) && series[1].include?(e)#edge1 + add_part_profile(entity, 1, profiles) + count += 1 + edge1 = true + break + elsif !edge3 && series[3].include?(s) && series[3].include?(e)#edge3 + add_part_profile(entity, 3, profiles) + count += 1 + edge3 = true + break + elsif !face2 && series[2].include?(s) && series[2].include?(e)#face2 + textured_surf(entity, @back_material, color2, color2, scale2, angle2) + count += 1 + face2 = true + break + end + } unless edge1 && edge3 && face2 + end + + if !color2.nil? && count == 5 || color2.nil? && count == 4 + break + end + } + end + + leaf + end + + uid = data.fetch("uid") + root = data.fetch("cp") + parts = get_parts(data) + part = parts.fetch(root, nil) + added = false + if part.nil? + part = Sketchup.active_model.entities.add_group + parts.store(root, part) + added = true + else + part.entities.each{|leaf| + leaf.erase! if leaf.valid? && leaf.get_attribute("sw", "typ") == "cp" + } + end + + part.set_attribute("sw", "uid", uid) + part.set_attribute("sw", "zid", data.fetch("zid")) + part.set_attribute("sw", "pid", data.fetch("pid")) + part.set_attribute("sw", "cp", root) + part.set_attribute("sw", "typ", "cp") + layer = data.fetch("layer") + if layer == 1 + part.layer = @door_layer + elsif layer == 2 + part.layer = @drawer_layer + end + + #开门和拉抽屉功能 + drawer_type= data.fetch("drw",0) + part.set_attribute("sw", "drawer", drawer_type) + if drawer_type == 73 or drawer_type == 74 #DR_LP/DR_RP + part.set_attribute("sw", "dr_depth",data.fetch("drd",0)) + end + if drawer_type == 70 + part.set_attribute("sw", "drawer_dir",Geom::Vector3d.parse(data.fetch("drv"))) + end + door_type= data.fetch("dor",0) + part.set_attribute("sw", "door", door_type) + if door_type == 10 or door_type == 15 + part.set_attribute("sw", "door_width",data.fetch("dow",0)) + part.set_attribute("sw", "door_pos",data.fetch("dop","F")) + end + + inst = nil + if data.include?("sid") + mirr = data.fetch("mr", nil) + mirr = mirr.nil? ? "" : "_" + mirr + file = "#{SUWood::suwood_path(V_StructPart)}/#{data.fetch("sid")}#{mirr}.skp" + puts file + if File.exist?(file) && File.size(file) > 0 + begin + defi = Sketchup.active_model.definitions.load(file) + inst = part.entities.add_instance(defi, ORIGIN) + inst.set_attribute("sw", "typ", "cp") + xrate = data.fetch("l") / inst.bounds.width.to_mm + yrate = data.fetch("w") / inst.bounds.height.to_mm + trans = Geom::Transformation::parse(data.fetch("trans")) + inst.transform!(Geom::Transformation.scaling(xrate, yrate, 1)) + inst.transform!(trans) + rescue => e + puts e + inst = nil + end + end + end + + if inst + leaf = part.entities.add_group + if data.fetch("typ") == 3 + center_o = Geom::Point3d.parse(data.fetch("co")) + center_r = Geom::Point3d.parse(data.fetch("cr")) + path = leaf.entities.add_line(center_o, center_r) + follow_me(leaf, data.fetch("obv"), path, nil) + else + obv = data.fetch("obv") + rev = data.fetch("rev") + series1 = [] + series2 = [] + create_face(leaf, obv, nil, nil, nil, series1)#obv + create_face(leaf, rev, nil, nil, nil, series2)#rev + add_part_edges(leaf, series1, series2, obv, rev) + end + leaf.set_attribute("sw", "typ", "cp") + leaf.set_attribute("sw", "virtual", true) + leaf.visible = false + finals = data.fetch("finals", []) + finals.each{|final| + next unless final.fetch("typ") == 2 + stretch = add_part_stretch(part, final) + if stretch + stretch.set_attribute("sw", "typ", "cp") + stretch.set_attribute("sw", "mn", final.fetch("mn")) + end + } + else + finals = data.fetch("finals", []) + finals.each{|final| + profiles = Hash.new + ps = final.fetch("ps", nil) + ps.each{|p| + p.fetch("idx").split(",").each{|idx| + profiles.store(idx.to_i, p) + } + } if ps + + leaf = + case final.fetch("typ") + when 1 then add_part_board(part, final, final.fetch("antiz", false), profiles) + when 2 then add_part_stretch(part, final) + when 3 then add_part_arc(part, final, final.fetch("antiz", false), profiles) + end + if leaf + leaf.set_attribute("sw", "typ", "cp") + leaf.set_attribute("sw", "mn", final.fetch("mn")) + else + puts "leaf is nil" + end + } + end + + if added && @unit_trans.key?(uid) + part.transform!(@unit_trans.fetch(uid)) + end + part.make_unique + part.definition.behavior.no_scale_mask = 127#限制所有方向的拉伸 + end + + def c05(data) #add_machining, 无需unit_trans + uid = data.fetch("uid") + parts = get_parts(data) + hardwares = get_hardwares(data) + unless @machinings.key?(uid) + @machinings.store(uid, []) + end + machinings = @machinings.fetch(uid) + + items = data.fetch("items") + items.each{|work| + cp = work.fetch("cp") + component = parts.key?(cp) ? parts.fetch(cp) : hardwares.fetch(cp, nil) + if component.nil? || component.deleted? + next + elsif work.fetch("trim3d") == 1 + work_trimmed(component, work) + else + special = work.fetch("special") + machining = component.entities.add_group + machinings << machining + machining.set_attribute("sw", "typ", "work") + machining.set_attribute("sw", "special", special) + p1 = Geom::Point3d.parse(work.fetch("p1")) + p2 = Geom::Point3d.parse(work.fetch("p2")) + if work.key?("tri") + tri = Geom::Point3d.parse(work.fetch("tri")) + p3 = Geom::Point3d.parse(work.fetch("p3")) + path = component.entities.add_line(p1, p3) + pts = [tri, tri.offset(p2 - p1), p1.offset(p1 - tri)] + face = machining.entities.add_face(pts) + elsif work.key?("surf") + path = component.entities.add_line(p1, p2) + surf = work.fetch("surf") + face = create_face(machining, surf) + else + path = component.entities.add_line(p1, p2) + za = (p2 - p1).normalize + dia = work.fetch("dia").mm + cir = machining.entities.add_circle(p1, za, dia / 2.0) + face = machining.entities.add_face(cir) + end + if special == 0 && work.fetch("cancel") == 0 + texture = get_texture("mat_machine") + face.material = texture + face.back_material = texture + end + face.followme(path) + path.erase! if path && path.valid? + end + } + end + + def c08(data) #add_hardware + hardwares = get_hardwares(data) + parts = get_parts(data) + + uid = data.fetch("uid") + zid = data.fetch("zid") + pid = data.fetch("pid") + items = data.fetch("items") + items.each{|item| + elem = nil + file = "#{SUWood::suwood_path(V_Hardware)}/#{item.fetch("uid")}.skp" + cp = item.fetch("cp") + ps = item.key?("ps") ? Geom::Point3d.parse(item.fetch("ps")) : nil + pe = item.key?("pe") ? Geom::Point3d.parse(item.fetch("pe")) : nil + if File.exist?(file) && File.size(file) > 0 + begin + defi = Sketchup.active_model.definitions.load(file) + elem = Sketchup.active_model.entities.add_instance(defi, ORIGIN) + if ps && pe + xrate = ps.distance(pe).to_mm / elem.bounds.width.to_mm + elem.transform!(Geom::Transformation.scaling(xrate, 1, 1)) + end + trans = Geom::Transformation::parse(item) + elem.transform!(trans) + rescue => e + puts e + elem = nil + end + end + if elem.nil? && ps && pe + elem = Sketchup.active_model.entities.add_group + path = Sketchup.active_model.entities.add_line(ps, pe) + sect = item.fetch("sect") + color = item.fetch("ckey") + follow_me(elem, sect, path, color) + elem.set_attribute("sw", "ckey",color) + end + + if elem + elem.set_attribute("sw", "uid", uid) + elem.set_attribute("sw", "zid", zid) + elem.set_attribute("sw", "pid", pid) + elem.set_attribute("sw", "cp", cp) + if item.key?("part") + elem.set_attribute("sw", "part", item.fetch("part")) + end + if item.key?("typ") + typ = item.fetch("typ") + elem.set_attribute("sw", "typ", typ) + if typ == "pull" + elem.layer = @door_layer + end + else + elem.set_attribute("sw", "typ", "hw") + end + hardwares.store(cp, elem) + end + + if elem && @unit_trans.key?(uid) + elem.transform!(@unit_trans.fetch(uid)) + end + + if elem && item.key?("work") + work = item.fetch("work") + part = parts.fetch(work.fetch("cp"), nil) + if part + work_trimmed(part, work) + end + end + } + end + + def work_trimmed(part, work) + leaves = [] + part.entities.each{|leaf| + leaves << leaf if leaf.get_attribute("sw", "typ") == "cp" + } + + leaves.each{|leaf| + next if leaf.deleted? + + attries = {} + leaf.attribute_dictionaries['sw'].each{|key, value| + attries.store(key, value) + } + + trimmer = part.entities.add_group + p1 = Geom::Point3d.parse(work.fetch("p1")) + p2 = Geom::Point3d.parse(work.fetch("p2")) + if work.key?("tri") + tri = Geom::Point3d.parse(work.fetch("tri")) + p3 = Geom::Point3d.parse(work.fetch("p3")) + path = part.entities.add_line(p1, p3) + pts = [tri, tri.offset(p2 - p1), p1.offset(p1 - tri)] + face = trimmer.entities.add_face(pts) + elsif work.key?("surf") + path = part.entities.add_line(p1, p2) + surf = work.fetch("surf") + face = create_face(trimmer, surf) + else + path = part.entities.add_line(p1, p2) + za = (p2 - p1).normalize + dia = work.fetch("dia").mm + cir = trimmer.entities.add_circle(p1, za, dia / 2.0) + face = trimmer.entities.add_face(cir) + end + + if work.fetch("differ", false) + texture = get_texture("mat_default") + face.material = texture + face.back_material = texture + end + + face.followme(path) + trimmed = trimmer.trim(leaf) + attries.each{|key, value| + trimmed.set_attribute("sw", key, value) + } if trimmed + path.erase! if path.valid? + trimmer.erase! if trimmer.valid? + + if work.fetch("differ", false) + texture = get_texture("mat_default") + trimmed.entities.each{|entity| + if entity.class == Sketchup::Face && (entity.material == texture || entity.back_material == texture) + entity.set_attribute("sw", "differ", true) + end + } + end + } + end + + def c12(data) #add_contour + @added_contour = true + if data.key?("surf") + surf = data.fetch("surf") + xaxis = Geom::Vector3d.parse(surf.fetch("vx")) + zaxis = Geom::Vector3d.parse(surf.fetch("vz")) + segs = surf.fetch("segs") + edges = [] + segs.each{|seg| + if seg.key?("c") + c = Geom::Point3d.parse(seg.fetch("c")) + r = seg.fetch("r").mm + a1 = seg.fetch("a1") + a2 = seg.fetch("a2") + n = seg.fetch("n") + edges.concat(Sketchup.active_model.entities.add_arc(c, xaxis, zaxis, r, a1, a2, n)) + else + s = Geom::Point3d.parse(seg.fetch("s")) + e = Geom::Point3d.parse(seg.fetch("e")) + edges << Sketchup.active_model.entities.add_line(s, e) + end + } + begin + Sketchup.active_model.entities.add_face(edges) + rescue => e + #do nothing + end + end + Sketchup.send_action "viewTop:" + Sketchup.send_action "viewZoomExtents:" + end + + def add_surf(data) + surf = data.fetch("surf") + create_face(Sketchup.active_model, surf) + end + + def c07(data) #add_dim + uid = data.fetch("uid") + dims = data.fetch("dims") + unless @dimensions.key?(uid) + @dimensions.store(uid, []) + end + dimensions = @dimensions.fetch(uid) + dims.each{|dim| + p1 = Geom::Point3d.parse(dim.fetch("p1")) + p2 = Geom::Point3d.parse(dim.fetch("p2")) + d = Geom::Vector3d.parse(dim.fetch("d")) + if @unit_trans.key?(uid) + trans = @unit_trans.fetch(uid) + p1.transform!(trans) + p2.transform!(trans) + d.transform!(trans) + end + + t = dim.fetch("t") + entity = Sketchup.active_model.entities.add_dimension_linear(p1, p2, d) + entity.has_aligned_text = true + entity.arrow_type = Sketchup::Dimension::ARROW_NONE + entity.text = t + dimensions << entity + } + end + + def c09(data) #del_entity #machining为part的子项,不需要单独删除 + sel_clear#清除所有选择 + + uid = data.fetch("uid") + typ = data.fetch("typ")#zid/cp/work/hw/pull/wall + oid = data.fetch("oid") + + if typ == "wall" + zones = get_zones(data) + zone = zones.fetch(oid, nil) + wall = data.fetch("wall") + zone.entities.each{|entity| + if entity.valid? && entity.class == Sketchup::Face && wall == entity.get_attribute("sw", "child") + entity.erase! + end + } + zone.entities.each{|entity| + if entity.valid? && entity.class == Sketchup::Edge && entity.faces.size <= 0 + entity.erase! + end + } + else + del_entities(get_zones(data), typ, oid) if ["uid", "zid"].include?(typ) + del_entities(get_parts(data), typ, oid) + del_entities(get_hardwares(data), typ, oid) + + @labels.entities.clear! if ["uid", "zid"].include?(typ) && @labels.valid? + @door_labels.entities.clear! if ["uid", "zid"].include?(typ) && @door_labels.valid? + + machinings = @machinings.fetch(uid, []) + machinings.delete_if{|entity| entity.deleted?} + end + + if typ == "uid" + @mat_type = MAT_TYPE_NORMAL + c0c(data)#del_dim + end + end + + def c0a(data) #del_machining #modified by xt, 20180613 + uid = data.fetch("uid") + typ = data.fetch("typ")#type is unit or source + oid = data.fetch("oid") + special = data.fetch("special", 1) + machinings = @machinings.fetch(uid, []) + machinings.each{|entity| + if entity.valid? && + (typ == "uid" || entity.get_attribute("sw", typ) == oid) && + (special == 1 || special == 0 && entity.get_attribute("sw", "special") == 0)#added by xt, 20180613 + entity.erase! + end + } + machinings.delete_if{|entity| entity.deleted?} + end + + def del_entities(entities, typ, oid) + entities.each{|key, entity| + if entity.valid? && (typ == "uid" || entity.get_attribute("sw", typ) == oid) + entity.erase! + end + } + entities.delete_if{|key, entity| entity.deleted?} + end + + def c0c(data) #del_dim + uid = data.fetch("uid") + if @dimensions.key?(uid) + dimensions = @dimensions.fetch(uid) + dimensions.each{|dim| dim.erase!} + @dimensions.delete(uid) + end + end + + def c15(data) #sel_unit + sel_clear + zones = get_zones(data) + zones.each{|zid, zone| + if zone.valid? + leaf = is_leaf_zone(zid, zones) + zone.hidden = leaf ? @hide_none : true + end + } + end + + def is_leaf_zone(zip, zones) + zones.each{|zid, zone| + if zone.valid? && zone.get_attribute("sw","zip") == zip + return(false) + end + } + true + end + + def set_children_hidden(uid, zid) + zones = get_zones({"uid" => uid}) + children = get_child_zones(zones, zid, true) + children.each {|child| + child_id = child.fetch("zid") + child_zone = zones.fetch(child_id) + child_zone.hidden = true + } + end + + def get_child_zones(zones, zip, myself = false) #本地运行 + children = [] + zones.each{|zid, entity| + if entity.valid? && entity.get_attribute("sw", "zip") == zip + grandchildren = get_child_zones(zones, zid, false) + child = {} + child.store("zid", zid) + child.store("leaf", grandchildren.length == 0) + children.push(child) + children.concat(grandchildren) + end + } + + if myself + child = {} + child.store("zid", zip) + child.store("leaf", children.length == 0) + children.push(child) + end + + children + end + + def c16(data) #sel_zone + sel_zone_local(data) + end + + #开门和拉抽屉功能 + def c10(data) #set_doorinfo + parts = get_parts(data) + doors = data.fetch("drs",[]) + doors.each {|door| + root=door.fetch("cp",0) + door_dir=door.fetch("dov","") + ps=Geom::Point3d.parse(door.fetch("ps")) + pe=Geom::Point3d.parse(door.fetch("pe")) + offset=Geom::Vector3d.parse(door.fetch("off")) + if root > 0 && parts.key?(root) + part = parts.fetch(root) + part.set_attribute("sw","door_dir",door_dir) + part.set_attribute("sw","door_ps",ps) + part.set_attribute("sw","door_pe",pe) + part.set_attribute("sw","door_offset",offset) + end + } + end + + def c1a(data) #open_doors + uid = data.fetch("uid") + parts = get_parts(data) + hardwares = get_hardwares(data) + mydoor = data.fetch("cp",0) + value = data.fetch("v",false) + parts.each{|root, part| + next unless mydoor == 0 or mydoor == root + + door_type=part.get_attribute("sw", "door",0) + next unless door_type > 0 + + is_open=part.get_attribute("sw","door_open",false) + next if is_open == value + + next unless door_type == 10 or door_type==15 + door_ps = part.get_attribute("sw","door_ps") + door_pe = part.get_attribute("sw","door_pe") + door_off = part.get_attribute("sw","door_offset") + if door_type == 10 #平开门 + if door_ps.nil? || door_pe.nil? || door_off.nil? + next + end + if @unit_trans.key?(uid) + trans = @unit_trans.fetch(uid) + door_ps.transform!(trans) + door_pe.transform!(trans) + door_off.transform!(trans) + end + trans_r = Geom::Transformation.rotation(door_ps, (door_pe-door_ps), 1.5708) #开90度 + trans_t = Geom::Transformation.translation(door_off) + trans_a = trans_r*trans_t + else + if door_off.nil? + next + end + if @unit_trans.key?(uid) + door_off.transform!(@unit_trans.fetch(uid)) + end + trans_a= Geom::Transformation.translation(door_off) + end + if is_open + trans_a.invert! + end + part.set_attribute("sw","door_open",!is_open) + part.transform!(trans_a) + hardwares.each{|key, hardware| + if hardware.get_attribute("sw", "part", nil) == root + hardware.transform!(trans_a) + end + } + } + end + + def c1b(data) #slide_drawers + uid = data.fetch("uid") + zones = get_zones(data) + parts = get_parts(data) + hardwares = get_hardwares(data) + + drawers={} + depths ={} + parts.each{|root, part| + drawer_type=part.get_attribute("sw", "drawer",0) + if drawer_type> 0 + if drawer_type == 70 #DR_DP + drawers.store( part.get_attribute("sw","pid"), part.get_attribute("sw","drawer_dir") ) + end + if drawer_type == 73 or drawer_type == 74 #DR_LP/DR_RP + depths.store( part.get_attribute("sw","pid"), part.get_attribute("sw","dr_depth",0)) + end + end + } + + offsets={} + drawers.each{|drawer,dir| + zone = zones.fetch(drawer, nil) + next if zone.nil? + + vector = dir + dr_depth=depths.fetch(drawer,300).mm + vector.length= dr_depth*0.9 + if @unit_trans.key?(uid) + vector.transform!(@unit_trans.fetch(uid)) + end + offsets.store(drawer, vector) + } + + value = data.fetch("v",false) + offsets.each{|drawer,vector| + zone = zones.fetch(drawer, nil) + next if zone.nil? + + is_open=zone.get_attribute("sw","drawer_open",false) + next if is_open == value + + trans_a = Geom::Transformation.translation(vector) + trans_a.invert! if is_open #==true + + zone.set_attribute("sw","drawer_open",!is_open) + parts.each{|root, part| + if part.get_attribute("sw", "pid") == drawer + part.transform!(trans_a); + end + } + + hardwares.each{|root, hardware| + if hardware.get_attribute("sw", "pid") == drawer + hardware.transform!(trans_a) + end + } + } + end + + def c17(data) #sel_elem + if @part_mode + sel_part_parent(data) + else + sel_zone_local(data) + end + end + + def c0d(data) #parts_seqs + parts = get_parts(data) + seqs = data.fetch("seqs") + seqs.each{ |d| + root=d.fetch("cp")#部件id + seq=d.fetch("seq")#顺序号 + pos=d.fetch("pos")#位置 + name=d.fetch("name",nil)#板件名称 + size=d.fetch("size",nil)#尺寸即长*宽*厚 + mat=d.fetch("mat",nil)#材料(包括材质/颜色),可能为空 + + e_part= parts.fetch(root,nil) + if e_part + e_part.set_attribute("sw","seq",seq) + e_part.set_attribute("sw","pos",pos) + if name + e_part.set_attribute("sw","name",name) + end + if size + e_part.set_attribute("sw","size",size) + end + end + } + end + + def c0e(data)#explode_zones + if @labels.valid? + @labels.entities.clear! + else + @labels= Sketchup.active_model.entities.add_group + end + + if @door_labels.valid? + @door_labels.entities.clear! + else + @door_labels= Sketchup.active_model.entities.add_group + end + + uid = data.fetch("uid") + zones = get_zones(data) + parts = get_parts(data) + hardwares = get_hardwares(data) + jzones = data.fetch("zones") + jzones.each{|zone| + zoneid = zone.fetch("zid") + offset = Geom::Vector3d.parse(zone.fetch("vec")) + if @unit_trans.key?(uid) + offset.transform!(@unit_trans.fetch(uid)) + end + trans_a = Geom::Transformation.translation(offset) + if zones.key?(zoneid) + azone = zones.fetch(zoneid) + azone.transform!(trans_a) + end + } + + jparts = data.fetch("parts") + jparts.each{|jpart| + pid=jpart.fetch("pid") + offset = Geom::Vector3d.parse(jpart.fetch("vec")) + if @unit_trans.key?(uid) + offset.transform!(@unit_trans.fetch(uid)) + end + trans_a = Geom::Transformation.translation(offset) + parts.each{|root, part| + if part.get_attribute("sw", "pid") == pid + part.transform!(trans_a) + end + } + hardwares.each{|root, hardware| + if hardware.get_attribute("sw", "pid") == pid + hardware.transform!(trans_a) + end + } + } + + if data.fetch("explode", false)#part seq text + parts.each{|root, part| + center=part.bounds.center.clone + pos=part.get_attribute("sw","pos",1) + if pos == 1 then vector=Geom::Vector3d.new(0, -1, 0)#F + elsif pos == 2 then vector=Geom::Vector3d.new(0, 1, 0)#K + elsif pos == 3 then vector=Geom::Vector3d.new(-1, 0, 0)#L + elsif pos == 4 then vector=Geom::Vector3d.new(1, 0, 0)#R + elsif pos == 5 then vector=Geom::Vector3d.new(0, 0, -1)#B + else vector=Geom::Vector3d.new(0, 0, 1)#T + end + vector.length = 100.mm + if @unit_trans.key?(uid) + vector.transform!(@unit_trans.fetch(uid)) + end + ord_seq=part.get_attribute("sw","seq",0) + if part.layer == @door_layer + text =@door_labels.entities.add_text ord_seq.to_s, center, vector + else + text=@labels.entities.add_text ord_seq.to_s, center, vector + end + text.material="red" + } + end + end + + def sel_local(obj) + uid = obj.get_attribute("sw", "uid") + zid = obj.get_attribute("sw", "zid") + typ = obj.get_attribute("sw", "typ") + pid = obj.get_attribute("sw", "pid", -1) + cp = obj.get_attribute("sw", "cp", -1) + params = {} + params.store("uid", uid) + params.store("zid", zid) + if typ == "zid" + if @@selected_uid == uid && @@selected_obj == zid + return + end + elsif typ == "cp"#part or slide + if @@selected_uid == uid && (@@selected_obj == pid || @@selected_obj == cp) + return + end + else + sel_clear + return + end + + if typ == "cp" && @part_mode + params.store("pid", pid) + params.store("cp", cp) + sel_part_local(params) + else + params.store("pid", -1) + params.store("cp", -1) + sel_zone_local(params) + end + set_cmd("r01", params)#select_client + end + + def sel_zone_local(data) #本地运行 + sel_clear + uid = data.fetch("uid") + zid = data.fetch("zid") + zones = get_zones(data) + parts = get_parts(data) + hardwares = get_hardwares(data) + children = get_child_zones(zones, zid, true)#children中包括自己 + children.each{|child| + childid = child.fetch("zid") + childzone = zones.fetch(childid) + leaf = child.fetch("leaf")#没有下级区域 + parts.each{|v_root, part| + if part.get_attribute("sw", "zid") == childid + textured_part(part, true) + end + } + + hardwares.each{|v_root, hw| + if hw.get_attribute("sw", "zid") == childid + textured_hw(hw, true) + end + } + + if !leaf || @hide_none + childzone.hidden = true + else + childzone.hidden = false + childzone.entities.each{|face| + if face.class == Sketchup::Face + textured_face(face, true) + end + } + end + + if childid == zid + @@selected_uid = uid + @@selected_obj = zid + @@selected_zone = childzone + end + } + + if @@selected_zone && Module.const_defined?("SuwoodResourceLibrary") + ref_v = SuwoodResourceLibrary::Browser.get_current_dialog#获取当前资源管理器ref_v + corner = @@selected_zone.get_attribute("sw", "cor", 0) + poses = [] + if ref_v == 2102#挂衣杆 + poses = + case corner + when 1 then [51,52,55,56]#常规转角 + when 2 then [53]#直拼转角, 宽向划分 + when 3 then [54]#直拼转角, 长向划分 + when 4 then [57]#斜拼转角 + else [51]#一字型 + end + elsif ref_v == 2103#灯带 + poses = + case corner + when 1 then [51,52,53,54]#常规转角 + when 2 then [53]#直拼转角, 宽向划分 + when 3 then [54]#直拼转角, 长向划分 + when 4 then [57]#斜拼转角 + else [51]#一字型 + end + end + if poses.length > 0 + SuwoodResourceLibrary::Browser.suwood_refresh_data(ref_v, poses)#刷新pos数据 + end + end + end + + def sel_part_parent(data)#from server + sel_clear + zones = get_zones(data) + parts = get_parts(data) + hardwares = get_hardwares(data) + + uid = data.fetch("uid") + zid = data.fetch("zid") + pid = data.fetch("pid") + parted = false + parts.each{|v_root, part| + if part.get_attribute("sw", "pid") == pid + textured_part(part, true) + @@selected_uid = uid + @@selected_obj = pid + parted = true + end + } + hardwares.each{|v_root, hw| + if hw.get_attribute("sw", "pid") == pid + textured_hw(hw, true) + end + } + + children = get_child_zones(zones, zid, true) + children.each{|child| + childid = child.fetch("zid") + childzone = zones.fetch(childid) + leaf = child.fetch("leaf")#没有下级区域 + if leaf && childid == zid + unless @hide_none + childzone.hidden = false + childzone.entities.each{|face| + if face.class == Sketchup::Face + selected = face.get_attribute("sw", "child") == pid + textured_face(face, selected) + if selected + @@selected_uid = uid + @@selected_obj = pid + end + end + } + end + elsif !leaf && childid == zid + unless parted + childzone.hidden = false + childzone.entities.each{|face| + if face.class == Sketchup::Face && face.get_attribute("sw", "child") == pid + textured_face(face, true) + @@selected_uid = uid + @@selected_obj = pid + end + } + end + elsif leaf && !@hide_none + childzone.hidden = false + childzone.entities.each{|face| + if face.class == Sketchup::Face + textured_face(face, false) + end + } + else#not leaf or hidden leaf + childzone.hidden = true + end + } + end + + def sel_part_local(data)#called by client directly + sel_clear + parts = get_parts(data) + hardwares = get_hardwares(data) + + uid = data.fetch("uid") + cp = data.fetch("cp") + if parts.key?(cp) + part = parts.fetch(cp) + if part && part.valid? + textured_part(part, true) + end + @@selected_part = part + elsif hardwares.key?(cp) + hw = hardwares.fetch(cp) + if hw && hw.valid? + textured_hw(hw, true) + end + end + @@selected_uid = uid + @@selected_obj = cp + end + + def sel_clear() + @@selected_uid = nil + @@selected_obj = nil + @@selected_zone = nil + @@selected_part = nil + @selected_faces.each{|face| textured_face(face, false) if face && face.valid?}; @selected_faces.clear + @selected_parts.each{|part| textured_part(part, false) if part && part.valid?}; @selected_parts.clear + @selected_hws.each {|hw | textured_hw( hw, false) if hw && hw.valid?}; @selected_hws.clear + end + + def textured_face(face, selected) + @selected_faces << face if selected + + color = selected ? "mat_select" : "mat_normal" + texture = get_texture(color) + face.material = texture + if @back_material or texture.alpha < 1 + face.back_material = texture + else + face.back_material = nil + end + end + + def textured_part(part, selected) + def face_color(face, leaf) + color = nil + if face.get_attribute("sw", "differ", false) + color = "mat_default" + else + if @mat_type == MAT_TYPE_OBVERSE + type = face.get_attribute("sw", "typ") + if type == "o" || type == "e1" then color = "mat_obverse" + elsif type == "e2" then color = "mat_thin" + elsif type == "r" || type == "e0" then color = "mat_reverse" + end + end + color = face.get_attribute("sw", "ckey") if color.nil? + color = leaf.get_attribute("sw", "ckey") if color.nil? + end + color + end + + part.entities.each{|leaf| + if leaf.class == Sketchup::ComponentInstance && leaf.get_attribute("sw", "typ") == "cp" + break + end + } + @selected_parts << part if selected + part.entities.each{|leaf| + next unless leaf.class == Sketchup::ComponentInstance || leaf.class == Sketchup::Group + next if leaf.class == Sketchup::ComponentInstance && leaf.get_attribute("sw", "typ") != "cp"#五金 + next if leaf.get_attribute("sw", "typ") == "work" + next if leaf.get_attribute("sw", "typ") == "pull" + + if leaf.class == Sketchup::ComponentInstance #模型部件 + leaf.visible = !(selected || @mat_type == MAT_TYPE_NATURE); next + elsif leaf.get_attribute("sw", "virtual", false)#虚拟部件(模型部件在选择状态时的替代品) + leaf.visible = (selected || @mat_type == MAT_TYPE_NATURE) + end + + nature = + if selected + "mat_select" + elsif @mat_type == MAT_TYPE_NATURE + case leaf.get_attribute("sw", "mn") + when 1 then "mat_obverse" #门板 + when 2 then "mat_reverse" #柜体 + when 3 then "mat_thin" #背板 + end + else + nil + end + + leaf.entities.each{|entity| + if entity.class == Sketchup::Face + color = nature.nil? ? face_color(entity, leaf) : nature + textured_surf(entity, @back_material, color) + elsif entity.class == Sketchup::Group + entity.entities.each{|entity2| + if entity2.class == Sketchup::Face + color = nature.nil? ? face_color(entity2, leaf) : nature + textured_surf(entity2, @back_material, color) + end + } + end + } + } + end + + def textured_surf(face, back_material, color, saved_color = nil, scale_a = nil, angle_a = nil) + def normalize_uvq(uvq); uvq[0] /= uvq[2]; uvq[1] /= uvq[2]; uvq[2] = 1.0; uvq end + + def rotate_texture(face, _scale, _angle, front = true) + scale = _scale.nil? ? 1.0 : _scale + angle = _angle.nil? ? 0.0 : _angle + material = front ? face.material : face.back_material + if material && material.texture + origin = face.outer_loop.vertices[0].position + axes = face.normal.axes + samples = [] + samples << origin # 0,0 | Origin + samples << origin.offset(axes.x) # 1,0 | Offset Origin in X + samples << origin.offset(axes.x + axes.y) # 1,1 | Offset X in Y + samples << origin.offset(axes.y) # 0,1 | Offset Origin in Y + writer = Sketchup.create_texture_writer + helper = face.get_UVHelper(front, !front, writer) + scale_x = Geom::Vector3d.new(axes.x); scale_x.length = scale #scale/25.4 + scale_y = Geom::Vector3d.new(axes.y); scale_y.length = scale #scale/25.4 + #axes.x.length=1 + #axes.y.length=1 + #axes.z.length=1 + points_3d = []# XYZ 3D coordinates + points_uv = []# UVQ 2D coordinates + [0, 1, 2, 3].each{|i| + v_3d = Geom::Vector3d.new; v_3d += scale_x if [1, 2].include?(i); v_3d += scale_y if [2, 3].include?(i) + v_uv = Geom::Vector3d.new; v_uv += axes.x if [1, 2].include?(i); v_uv += axes.y if [2, 3].include?(i) + points_3d << origin.offset(v_3d) + point_uv = origin.offset(v_uv) + points_uv << (front ? helper.get_front_UVQ(point_uv) : helper.get_back_UVQ(point_uv)) + } + + trans_a = Geom::Transformation.rotation(points_3d[0], face.normal, angle) + points = [points_3d[0], points_uv[0]] + (1..3).each{|i| + points << points_3d[i].transform(trans_a) + points << points_uv[i] + } + #puts points + face.position_material(material, points, front) + end + face.set_attribute("sw", "rt", true)#设置纹理已旋转 + end + + begin + if saved_color + face.set_attribute("sw", "ckey", saved_color) + face.set_attribute("sw", "scale", scale_a) if scale_a + face.set_attribute("sw", "angle", angle_a) if angle_a + end + + texture = get_texture(color) + face.material = texture + if back_material or texture.alpha < 1 + face.back_material = texture + else + face.back_material = nil + end + + if face.get_attribute("sw", "ckey") == color + scale = face.get_attribute("sw", "scale") + angle = face.get_attribute("sw", "angle") + if (scale || angle) && face.get_attribute("sw", "rt").nil?#纹理是否已旋转(反复贴图只需首次旋转) + rotate_texture(face, scale, angle, true) + if back_material or texture.alpha < 1 + rotate_texture(face, scale, angle, false) + end + end + end + rescue => e + puts e.backtrace + end + end + + def textured_hw(hw, selected) + return if hw.class == Sketchup::ComponentInstance + @selected_hws << hw if selected + + color = selected ? "mat_select" : hw.get_attribute("sw", "ckey") + texture = get_texture(color)#TODO angled the texture + hw.entities.each{|entity|#Group are machinings + if entity.class == Sketchup::Face + entity.material = texture + entity.back_material = texture + end + } + end + + def c18(data) #hide_door + @door_layer.visible = !data.fetch("v") + #@drawer_layer.visible= !params.fetch("v") + @door_labels.visible = !data.fetch("v") if @door_labels.valid? + end + + def c28(data) #hide_drawer + #@door_layer.visible = !params.fetch("v") + @drawer_layer.visible= !data.fetch("v") + @door_labels.visible = !data.fetch("v") if @door_labels.valid? + end + + def show_message(data) + Sketchup.set_status_text(data.fetch("message")) + end + + def c0f(data) #view_front + Sketchup.send_action "viewFront:" + Sketchup.send_action "viewZoomExtents:" + Sketchup.active_model.active_view.invalidate + end + + def c23(data) #view_left + Sketchup.send_action "viewLeft:" + Sketchup.send_action "viewZoomExtents:" + Sketchup.active_model.active_view.invalidate + end + + def c24(data) #view_right + Sketchup.send_action "viewRight:" + Sketchup.send_action "viewZoomExtents:" + Sketchup.active_model.active_view.invalidate + end + + def c25(data) #view_back + Sketchup.send_action "viewBack:" + Sketchup.send_action "viewZoomExtents:" + Sketchup.active_model.active_view.invalidate + end + + def c00(data)#add_folder + if data.fetch("ref_v", 0) > 0 + SuwoodResourceLibrary::FolderPathData.init_folder_data(data) + end + end + + def c01(data) #edit_unit, 仅当场景中创建时有效 + unit_id = data.fetch("unit_id") + if data.key?("params") + params = data.fetch("params") + if params.key?("trans") + jtran = params.delete("trans") + trans = Geom::Transformation::parse(jtran) + @unit_trans.store(unit_id, trans) + sleep(0.5) + end + if @unit_param.key?(unit_id) + values = @unit_param.fetch(unit_id) + params.each{|key, value| values.store(key, value)} + params = values + end + @unit_param.store(unit_id, params) + end + end + + def c14(data) #pre_save_pixmap + sel_clear + c0c(data) #del_dim + c0a(data) #del_machining + zones = get_zones(data) + zones.values.each{|zone| + if zone && zone.valid?#当前unit所有zone + zone.hidden = true + end + } + Sketchup.send_action "viewFront:" + Sketchup.send_action 10513#X透光 + Sketchup.send_action "renderTextures:" + Sketchup.active_model.active_view.zoom_extents + Sketchup.active_model.active_view.invalidate + end + + def c13(data) #save_pixmap + uid = data.fetch("uid") + path = data.fetch("path") + keys = { + :filename => path, + :width => 320, + :height => 320, #240 + :antialias => true, + :compression => 1.0, + :transparent => false + } + Sketchup.active_model.active_view.write_image(keys) + + if data.fetch("batch", nil) + c09(data) #del_entity + Sketchup.send_action 10513#X透光 + Sketchup.send_action "renderTextures:" + end + params = {:uid => uid} + set_cmd("r03", params) #"finish_pixmap" + end + + def follow_me(container, surface, path, color, scale = nil, angle = nil, reverse_face = true, series = nil, saved_color = nil)#path is an edge or an array of edge + face = create_face(container, surface, color, scale, angle, series, reverse_face, @back_material, saved_color) + normal = face.normal.normalize + + face.followme(path) + container.entities.each{|myentity| + if myentity.class == Sketchup::Edge + myentity.hidden = true + end + } + if path.class == Sketchup::Edge + container.entities.erase_entities(path) if path.valid? + else + path.each{|p|container.entities.erase_entities(p) if p.valid?} + end + + normal + end + + def create_face(container, surface, color = nil, scale = nil, angle = nil, series = nil, reverse_face = false, back_material = true, saved_color = nil, type = nil) + begin + segs = surface.fetch("segs") + edges = create_edges(container, segs, series) + face = container.entities.add_face(edges) + raise "无法创建Sketchup面对象!" if face.nil? + + zaxis = Geom::Vector3d::parse(surface.fetch("vz")) + if series #part surf + xaxis = Geom::Vector3d::parse(surface.fetch("vx")) + + if face.normal.samedirection?(zaxis) == reverse_face + face = face.reverse! + #axes = face.normal.axes + #puts "axes nil" if axes.nil? + #puts "done\t#{reverse_face ? "true":"false"}\t#{surface.fetch("p")}\t#{segs[0][0]}\t#{surface.fetch("vx")}\t#{surface.fetch("vz")}\t#{axes[0]}\t#{axes[2]}\t#{angle}\t#{xaxis.angle_between(axes[0])}" + else + #axes = face.normal.axes + #puts "axes nil" if axes.nil? + #puts "none\t#{reverse_face ? "true":"false"}\t#{surface.fetch("p")}\t#{segs[0][0]}\t#{surface.fetch("vx")}\t#{surface.fetch("vz")}\t#{axes[0]}\t#{axes[2]}\t#{angle}\t#{xaxis.angle_between(axes[0])}" + end + elsif reverse_face && face.normal.samedirection?(zaxis) + face = face.reverse! + end + + face.set_attribute("sw", "typ", type) unless type.nil? + if color + textured_surf(face, back_material, color, saved_color, scale, angle) + else + textured_surf(face, back_material, "mat_normal") + end + + face#return + rescue => e + segs.each_index{|index| + segs[index].each{|point| puts point} + } + puts e.backtrace + end + end + + #用于创建当前ContourSurface中所有ContourSegments的SU Edges + def create_edges(container, segments, series = nil) + entities = container.entities + seg_pts = Hash.new + segments.each_index{|index| + pts = [] + segments[index].each{|point| pts << Geom::Point3d.parse(point)} + seg_pts.store(index, pts) + } + edges = []; segments.each_index{|this_i| + pts_i = seg_pts.fetch(this_i) + pts_p = seg_pts.fetch(this_i == 0 ? segments.size - 1 : this_i - 1) + pts_n = seg_pts.fetch(this_i == segments.size - 1 ? 0 : this_i + 1) + if pts_i.size > 2 + if pts_p.size > 2 + prev_p = pts_p[-1] + this_p = pts_i[ 0] + if prev_p != this_p + edges << entities.add_line(prev_p, this_p) + end + end + edges.concat(entities.add_edges(pts_i)) + series << pts_i if series + else + point_s = pts_p.size > 2 ? pts_p[-1] : pts_i[ 0] + point_e = pts_n.size > 2 ? pts_n[ 0] : pts_i[-1] + edges << entities.add_line(point_s, point_e) + series << [point_s, point_e] if series + end + } + edges + end + + def create_paths(container, segments) + entities = container.entities + edges = []; segments.each{|seg| + if seg.fetch("c", nil).nil? + s = Geom::Point3d.parse(seg.fetch("s")) + e = Geom::Point3d.parse(seg.fetch("e")) + edges << entities.add_line(s, e) + else + c = Geom::Point3d.parse(seg.fetch("c")) + x = Geom::Vector3d.parse(seg.fetch("x")) + z = Geom::Vector3d.parse(seg.fetch("z")) + r = seg.fetch("r") + a1 = seg.fetch("a1") + a2 = seg.fetch("a2") + n = seg.fetch("n") + edges.concat(entities.add_arc(c, x, z, r, a1, a2, n)) + end + } + edges + end + + def scaled_start + if @@scaled_zone || @@selected_zone.nil? + return + end + + select = @@selected_zone + if select.valid? + uid = select.get_attribute("sw", "uid") + zid = select.get_attribute("sw", "zid") + set_children_hidden(uid, zid)#当前所有区域不可见 + + @@scaled_zone = select.copy + @@scaled_zone.hidden = false + @@scaled_zone.make_unique + @@scaled_zone.definition.behavior.no_scale_mask = 120#限制除X/Y/Z三个方向外的其他方向拉伸 + end + end + + def scaled_finish + if @@scaled_zone.nil? + return + end + + bounds = @@scaled_zone.bounds + select = @@selected_zone + uid = select.get_attribute("sw", "uid") + zid = select.get_attribute("sw", "zid") + if bounds.min != select.bounds.min || bounds.max != select.bounds.max + points = [] + for i in 0..7 + points << bounds.corner(i).to_s("mm", 1) + end + params = {} + params.store("method", SUZoneResize) + params.store("uid", uid) + params.store("zid", zid) + params.store("points", points) + set_cmd("r00", params) + else + data = {} + data.store("uid", uid) + data.store("zid", zid) + sel_zone_local(data) + end + @@scaled_zone.erase! + @@scaled_zone = nil + end + + def self.selected_uid + @@selected_uid + end + + def self.selected_zone + @@selected_zone + end + + def self.selected_part + @@selected_part + end + + def self.selected_obj + @@selected_obj + end + + def self.server_path + @@server_path + end + + def self.default_zone + @@default_zone + end + end +end \ No newline at end of file diff --git a/ruby/ruby/SUWLoad.rb b/ruby/ruby/SUWLoad.rb new file mode 100644 index 0000000..50dfe39 --- /dev/null +++ b/ruby/ruby/SUWLoad.rb @@ -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') + diff --git a/ruby/ruby/SUWMenu.rb b/ruby/ruby/SUWMenu.rb new file mode 100644 index 0000000..c9d8c5a --- /dev/null +++ b/ruby/ruby/SUWMenu.rb @@ -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 \ No newline at end of file diff --git a/ruby/ruby/SUWObserver.rb b/ruby/ruby/SUWObserver.rb new file mode 100644 index 0000000..bf3065a --- /dev/null +++ b/ruby/ruby/SUWObserver.rb @@ -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 \ No newline at end of file diff --git a/ruby/ruby/SUWUnitContTool.rb b/ruby/ruby/SUWUnitContTool.rb new file mode 100644 index 0000000..bb4ff3b --- /dev/null +++ b/ruby/ruby/SUWUnitContTool.rb @@ -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 \ No newline at end of file diff --git a/ruby/ruby/SUWUnitFaceTool.rb b/ruby/ruby/SUWUnitFaceTool.rb new file mode 100644 index 0000000..a11cafe --- /dev/null +++ b/ruby/ruby/SUWUnitFaceTool.rb @@ -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 diff --git a/ruby/ruby/SUWUnitPointTool.rb b/ruby/ruby/SUWUnitPointTool.rb new file mode 100644 index 0000000..626a8cb --- /dev/null +++ b/ruby/ruby/SUWUnitPointTool.rb @@ -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 diff --git a/ruby/ruby/SUWZoneDiv1Tool.rb b/ruby/ruby/SUWZoneDiv1Tool.rb new file mode 100644 index 0000000..81d6756 --- /dev/null +++ b/ruby/ruby/SUWZoneDiv1Tool.rb @@ -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 diff --git a/ruby/ruby/icon/cursor_move.svg b/ruby/ruby/icon/cursor_move.svg new file mode 100644 index 0000000..62b1184 --- /dev/null +++ b/ruby/ruby/icon/cursor_move.svg @@ -0,0 +1,33 @@ + + + + cursor_move + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruby/ruby/icon/order_quote.png b/ruby/ruby/icon/order_quote.png new file mode 100644 index 0000000000000000000000000000000000000000..97d26daadac764c34c01a8f85b09188845581f86 GIT binary patch literal 398 zcmV;90df9`P)=DEwkr@Zbx>f#=^y_5savGKLSLrI;B?HF;6Q_JzvEhXZ7#JP^0K$}VIDgm|wg3PC07*qoM6N<$f;T;({{R30 literal 0 HcmV?d00001 diff --git a/ruby/ruby/icon/unit_delete.png b/ruby/ruby/icon/unit_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..a9cddf83785ff110ffdbea64d730cbc7aca7ed7b GIT binary patch literal 967 zcmV;&133JNP)kAdjiXc`94cfHzk*N5h zK0<^(^F|eH%{HBCx{m!Q+H`k7v z6o#U}%p(^I<0k0~IIk~v;WT5++Vol4HECwJML)i_Hi{lGaJB8*P1{rpkm9PjvlF8C z=rl||@AWUpTQbWa70j7GRWYf&{vpoa?R+ye#_*-%`1)Jp3b1>tU#BF+#%|4;t6Q_! zjG6J1RkhyolEYWk4FVzf<>|@I`~Fh^#Z^9r5UpjpF?g=zb8T#tR+T`AZ+^FZ zr(5`ho-OJcyUu`+;o*;$lE(u4{4OjB$7w6OTcy9y1gQhnSHV zCs`KJ7w{88T10<9KcPVB6C3cvhP;gjQF?SYR%E73lmg?*gF603MJC>!!3Tu_%eLd* z-xMB?DxID_7fw~y$#$)+SsoJA*P7(Du`u+^j#RMhk7#*J(b1_wpdU6zn`}R5e=ZLR z>-9!?EkZ!L&@(&sgVJR5_q%c5J`BJAz3tk_8PKyu-Sp?3MiBB79RE-}do~ip=_bLe z(Z8F(h)}4TzE|!}z0t64^&MX`0!mzSMkyzOUqC54i zi>{OwRMdwc2?`=2BKj0j)CaL5LqAk>G5laep_Gx3rK9o^ODvn^JkHB@wsVQIV0D^n zGwc89;d$Tp|9hV25hW2lIEiL=Kn+Sk7tL7`MOR~I55GCu%+zg{)xFkC*p#i@A z%;9i#Q!t68)vYHtmPP?|WmmgH8g}~eEq1c2(3X(tSh`T}Ld}a0o<5&{om^4U5&=wg zTxlutywAU%WtSIN)1;aMtLfiDly3G^HYnZHFuyJ=_q5|{^&x_mSD1V3mh|*p@3@S8 z{t#|AyK%0rHN}OpwT=n9Ow3R;%@C}QeNnw^W6RNMMoi7k(&`-t9N}u zU2Bin{Lw41lMGBBYoPv_HWoCx4i%tdh7})oY(thMC6Y5eZpO*R>tvrG!jfVXxPU;| zDJj&eH&NuwiF;2fK)c?Ao4Yrt0A`~O=y-`ckIrLr@mURWvf_EF^z)J>k@6Z=Wv*z` z(tI?g&w^3lG1ds;iI`R$d}220%x0rLACL(H5_Lq9Wf+KzwowLR#&iT07A5B1=HkNd z!0tHZesZ~dW_TPiRNMFlM{YIaecrOf%WrdWkrDmLTv2kI^;l9pGD4};RT5XH;j`G;fui^l}SlQzWn;#>a)r}Z>mn6j6I5_MlQ_O~K<1q(-@9%9_FRqiQGynhq07*qoM6N<$g8yW+`~Uy| literal 0 HcmV?d00001 diff --git a/ruby/ruby/icon/unit_face.png b/ruby/ruby/icon/unit_face.png new file mode 100644 index 0000000000000000000000000000000000000000..a41eb57ee9667005f9eedd85d847ee01a51a2cbb GIT binary patch literal 1100 zcmV-S1he~zP)V#5NG-Z3zVy0%`Igcem#e0;#Lo*(I1Mu?IlDMC?N{+C3{K4=T^&wYX74|#CfJLL?q zsU*(#5AweaoW;4LMTAf}-J^H#!<8X7<*<>5#(G4Je|6Ftt7$pxa`dC?s-)J8h{a$t zvj}2x0)-9jd^vNQDEzw%J;Pme80&%|Asc7XwMe*XfgxT4`xYglE#`Fz2vmRTMsChp zA^_Q!+UBpFua_+Ko0E6qt(a`=%1Xw~v1zzi4q2%W!8kaESY_mG#=~+8EZ2DB$M>7p zSKtFCi5xXM_G3t)LX*A}&z0mOtLg;m^hI8~toCy_nT4(O6{V=EZ~5Yhmp@9FvCYv_ zGqw|c3Fhe_qAxfS#oBUIl@`ErV;qTbO7AnZDty`2W+Zy=`*vlbI6nh7$1Syqv5}aW zoqLJgfPlACVq~Tzpy7xPB9hopZIW6A-JTp=`1B~;oJZ!D;6Y?0tDy*y`t=ExidXe1 z@H~gUehQDIdMn5u1QnYF$)Bv0f@O^`Ugu%8{x3BTM@4wsgEp3`i2w3g4RQGU7cesV z6+$c&wxw-Iuu@Ic-km;ec=w|Jz&AhhHQ!Q2gJF#Lqj$9$&a>&%^o>kFBBmGCyktQQ zC`yl!QO+a|4QkBX=l=87)TRjxzw{tnF8kGI6)mX=S!93zDKYPH5lPOrU%4LeT8i%2;k*|=;P?PhQFoG0uc$W&u>pMt!rpS%<{YYgyT!XYe=D!~ zuod0?x5iCo>k(mW!@r6pDE7P>789XppJV3pjvp!^3$jwYr7FN=wxYeuh^Fs4xdk^j zaL?}CZSQRIo?HPFtRULGH74RsyOYt{=3EM4OsE#s7P--6w!UHSY`VPc4B%f?9OI9V S3T2-F00005`+4ts_wGj#La-W9wpMjfj>^VIg=&VTbO7w`#)^)m zEJOh)TWfKlF2g>{3zZzp$e$`J?r{5uw!^m!sfkopoFR_HHb1@Oz(I0 z;GA=C{7x|RC~2v2%>%TDnq8%T4Q}qtwGgX$t? z)`N+*mnhG#*Jw-q=j4YQw^wBx49(&E)gd&0-#0fa5XtSSNOj68)%+~_&mW^(j8@bM z0(r1VPo1i@E@>LizO!c(7hImX2t`vQGgC4y&0=9(2XyxYmW;tR@FNcVd6n9|zX6uP z3G(5gt<=LNeWEXJc+utIT_b)9ayjcylOwd=$bw-~d=>zHS!Y1?rk7DS+=Y*_O|U4X zaJHYodt3HX_NaiIOhq&t0|cc~;*%+**qF0oZ1Vt3%%AVg&&R9PyK(Gv79QKS87c*b z2cmMwBs2_O7Y5W;Nvz1UqG_^d)y^ZurmEMxWk#K5`xX=SmER9}dKykVvj=sRJD|zd z;L2xj;@#s zL)PsS4~X9^8}mdBR>{*7vB~9}Xu$kCC@(f9uK!!~Vt&axTBXn-tT zI&$4TvZ_h`sgQU8L(@N9bB~IiaQ2cDw0@bW1Of2-ft!OsB*M_Y4|tkRcU%dJF`t|c z;SI-E=+)n|up<%yyk4NMZx+|PeHb0Rk|;Ju$EJ8;~9Jx zQQ%hIeLye>_yd5?$Ae+|LDQEoGt)#`YKD?6=Iep^$%Y#h*5Xa^6CD56lQr!1;n|aC z&^trJlWpWNH9ZZ6>4hMCOq#0BF1M%!HTj|1#-o!n;p5pVg+!~#kj9_g8k>l*3^Vk% zAnWpQIxR1d`go-h6KM({S*o2!3KF@0`@w=OuSx*^0f5wVcR;sS`2YX_07*qoM6N<$ Ef{9G;bN~PV literal 0 HcmV?d00001 diff --git a/ruby/ruby/icon/unit_point.png b/ruby/ruby/icon/unit_point.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca8671c1e411a92eaae4331ab63e9b3b6589d20 GIT binary patch literal 1039 zcmV+q1n~QbP)-y*cOi`+d*vJbw35gb;*-D!-skmBnamF7H0SnSRRl7&%)y64oh* zkOJ~oE^EX>eu>%Pel#ggBq&Hz2zRE+aL-`E>utAeK7y0_cZzBv7NE*6D3hi1ULPS^ zi^H?AAT2@Q515%0x5Qz_>BZ4cZg|J0?Sn&q%w&_ZrRJ~#RQY+2O1{=ts8;ZIZk9nF z6BSYF9p4Y*c++QdbLL4m2zk%aQ(qg(jzvImei}FN?i$SANQ3+7y3&o}?MilU^D$w3 zthi>wzM7*U1;Ap>5p^2Xb$5cD{${)HzAD5xu(GM{FS?W#5%XpZz zPC|Ds_Mz~6ix(N&IDsCI2ZF)-K0fhL&Cg^}Cc7Ko{cN1Q*{>%XtdlJFi=xrn=#;F-q4ZtQwZ0XY zeJ~fhZ=$;Kvc+t5C@nqp=Kq!`1a6reoBkSe*I)aP32x!=aY&Vq#%gWI;C?junMhHt zi;9a`^TEPBh=96n`N+=Pg2F6SSmV=G2RJsT`)NA2QXR2CFCCAIqD1D@tb{_k7DtNn zI1)O#BVvIUwm*fcgN2N9BvfgifK(!iPuO+r#EJs`+0)WkRP4!#DEK_pWd-ag0k4b$ zLT1sgB*%=sq``?INv z)@Ly~){JQr2alvIa4#IyF|95!1HkSTmuL9%I`#Z}U=K*c{*TBR4Hr)59rWEIn4S(e zn0Z39E7F;^T9({k(o~{mvKjidd0ZI7eSF8ujsdaE5$m$v7<5^uiU94ZH2iz8TuUxF zS&M_`mGk4GoWr$mf80gFA<8*Sg5xd8Aeg~RFNOSwtcHk7#s4MHey;OdPyf(($U%QK zmC(^+!F?j{nuxm1Y|CEMn7s;f%~%!?vllWWh9#IKo7n~#iCrpjLmS0A zWg!bd*}7LQD_qbp8{nE%G|AptRjR5gH6TYtV|VXxl#TG;k6v>(iS|a1Y(UYTz6MQZ zMyodzUb?zuq1tT9&!}Bi2#tzP>FqiL@(c9R8dJTF$3+9!8xy>Ip?DTCXFsSsH?}rE?~`sibExuGN95tip+}j-Yz;M!fStef)YXP2bt~ z@fWRD`j%IaSz{5P(WLW^>wkd2le2k+=s3_4AGU9Q6g>syn99tFkJV8QQcEBkP^=0S zztC6*ty+~Rb{=>Jt+mZ_-76nZ+5A?$K8xhEJ-IfiSiRDO71gV8Z26k`1GMXQg|Bzt2^_U@4zX?_6ahmRb;_q?7v!}wiN z4bBVW__^;etgg%Q9C;payMc*Gfa6Ho++l@`z0Yy~nKDceY$yb!@)rpFr()-p{?8QOHcgY2>7jSz3kB5aqF$Ib`jX>ZN(cYL8H7$UMg6F)hcxL@0d&&?D0uvKq z`22nlauGb=0ggK(brwqkVjs9$-{hG5Y)Hv`xx=Jy>TLJ<>Gk0VzdLqPrJLAIB~G+; zZAWTuFoDxrwJPx!5HrtTpgBD7!Aoz*HB1tKe*ygpemEIqT^|4d002ovPDHLkV1hy% B@G}4a literal 0 HcmV?d00001 diff --git a/ruby/ruby/icon/zone_div1.png b/ruby/ruby/icon/zone_div1.png new file mode 100644 index 0000000000000000000000000000000000000000..05194324bf74c796d4e175a68dfbd247835d08fc GIT binary patch literal 1121 zcmV-n1fKheP)w~vD&if|q?vc?u&h=Dt6;^iFmUTGY;W!N2ytx3)Musb zZ1NT54X0*Z-a@HFWaI@XN@8)|7fjmSJ~lat3YHkfDOP!2Jy^4b|AHWl9buR{#L|?)m`K zPa|ZWf=x|b)K>3pwCUm*Xq@;Hmy(`Pl1>L;GDT{RWIqE1Yjd#Tb?O+`yrZu@i?KWM z(#1@iS13VcpPX;|VINeF_Cti628;YsQYxGi5YV)P z(h`aW0~0eI09w(T9fR=4fEu!vB6d-qdP@bygt$X1t@mJ-UVsB7Zk?H(|AdP=L<^ec(K-32To20yW1p ziE^S_^2S{hjo<#dPH8F|$739@;cb3ZpKvlB12OyHzkog_vy^ErHpTxc5D$<1ZTYuQ nzp~GG+cNV!J;6&AsSSSvfgs7Qd4-JP00000NkvXXu0mjfv*QrP literal 0 HcmV?d00001 diff --git a/ruby/ruby/icon/zone_div11.png b/ruby/ruby/icon/zone_div11.png new file mode 100644 index 0000000000000000000000000000000000000000..9141632bfb9f331269d10560f95d631ede3c3947 GIT binary patch literal 1137 zcmV-%1djWOP) zQqrch1c-&DC@C+EeGp>9L!&7VO;efzx(W{{G)mQ0ViU0`khG!Fh(SsU+HetM6=B%L zz0T}3vt)){_R{V5a?bhxbH4BY&N(@U;CUYZ2ZXLSDm^w*M-s$4c8*I1aN^O~vRnQr zf)XHfy*U-Hr8JJr7|Ini8TR2}wc@Z^g$MduLL!`}yfOghNtB&(eLjYXtCVg)U8 zjV%tR&jO~G8Pr`GwTIATzsx;x)G&kReN6-=?jy7Lnsa&05x$*@zi9iq;`lS%5KKJo1c?ZTBeWTD zuH2oeCET8T!i~?bIq8TnSxIg(IsdAL2I*Y~l@uF2Zm~MzoFupG<>uuE#%LOPO+Img zp$z+NMtR%+j>v=7*uL zu}Ftgrp#<|Ej9sADy3m1 z4`vt3#j!dw_d-d1ySi5Y1yM2gM__@eRU}&(%*`9juvo32sNX=Kjqm(m)Z?++_@(_< zx1lDnvc0zY=*L|h8AVj<#0^hYp~`gzU>M8NUgcAkbq<2r_=?w8dD!-PP2I70rRfr( z>%DtFI%%l*_0Q5^HUp0z+hN+=1W8VV<2qoo4bGf-&+9J^Nnul +set PYTHONIOENCODING=utf-8 echo. echo =============================================== echo Socket JSON 传输系统 - Windows 测试工具 diff --git a/test_report.json b/test_report.json new file mode 100644 index 0000000..48583ca --- /dev/null +++ b/test_report.json @@ -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 \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 条" +} \ No newline at end of file diff --git a/test_socket.py b/test_socket.py index 49f029f..b3970ef 100644 --- a/test_socket.py +++ b/test_socket.py @@ -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 diff --git a/使用说明.md b/使用说明.md index f9e2f22..f7595bc 100644 --- a/使用说明.md +++ b/使用说明.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` - 依赖说明(无需额外包) + ## 🛡️ 安全注意事项 - 此系统仅用于学习和测试,不适用于生产环境