OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

15
dev/DWGViewer/.htaccess Normal file
View File

@@ -0,0 +1,15 @@
# WebAssembly 必须带正确的 MIME否则 instantiateStreaming 会失败
AddType application/wasm .wasm
AddType text/javascript .js .mjs
# wasm/图标体积大,允许长缓存(升级时改文件名或加 ?v=
<FilesMatch "\.(wasm|css|png|ico)$">
Header set Cache-Control "public, max-age=2592000"
</FilesMatch>
# 防下载:图纸源文件一律不允许直接 GET只能走 api/model.php校验短时令牌 + 解密)。
# 查看器已经会自动走令牌流程,所以封掉不影响正常使用。
# 如果你希望图纸可以直链下载,把下面这段删掉即可。
<FilesMatch "\.(dwg|dxf|enc|DWG|DXF)$">
Require all denied
</FilesMatch>

View File

@@ -0,0 +1,4 @@
# 禁止直接访问图纸文件双保险PHP 接口已校验令牌)
<FilesMatch "\.(dwg|dxf|enc)$">
Require all denied
</FilesMatch>

View File

@@ -0,0 +1,80 @@
<?php
/**
* DWG/DXF 查看器 - 图纸读取接口
* 校验令牌 → .enc 解密 / 原文件原样 → 流式返回。
* SECRET 必须与 token.php、upload.php 一致。
*/
$SECRET = 'be323d1b90bd2fda624083d4d1d716b7';
// 与 upload.php 保持一致的私有目录配置:图纸放 Web 目录外web 里根本没有文件可下载
$PRIVATE_DIR = '/volume1/dwgviewer_private';
function json_out($code, $obj) {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($obj, JSON_UNESCAPED_UNICODE);
exit;
}
$f = $_GET['f'] ?? '';
$t = $_GET['t'] ?? '';
$e = (int)($_GET['e'] ?? 0);
if (!$f || !$t || !preg_match('/\.(dwg|dxf|enc)$/i', $f)) json_out(400, ['error' => 'bad params']);
if (strpos($f, '..') !== false) json_out(400, ['error' => 'bad path']);
$sig = substr(hash_hmac('sha256', $f . '|' . $e, $SECRET), 0, 16);
if (!hash_equals($sig, $t) || time() > $e) json_out(403, ['error' => 'invalid or expired token']);
$root = realpath(__DIR__ . '/..');
if ($PRIVATE_DIR !== '') {
// 私有目录优先(嵌入链接写纯文件名即可);目录不存在时退回站内路径,
// 让留在 web 目录里的样例图纸按原路径继续可用(这些文件本来就允许直链,
// 退回不新增暴露面)。要保护的图纸必须移出 web 目录放进私有目录。
$full = realpath($PRIVATE_DIR . '/' . basename($f));
$ok = $full && strpos($full, realpath($PRIVATE_DIR)) === 0 && is_file($full);
if (!$ok) {
$full = realpath($root . '/' . $f);
$ok = $full && strpos($full, $root) === 0 && is_file($full);
}
} else {
$full = realpath($root . '/' . $f);
$ok = $full && strpos($full, $root) === 0 && is_file($full);
}
if (!$ok) json_out(404, ['error' => 'not found']);
$data = file_get_contents($full);
$origName = basename($f);
if (preg_match('/\.enc$/i', $f)) {
$nonce = substr($data, 0, 16);
$key = hash('sha256', $SECRET . $nonce, true);
$body = substr($data, 16);
$out = '';
for ($i = 0; $i < strlen($body); $i++) {
$out .= $body[$i] ^ $key[$i % strlen($key)];
}
// 加密体头部藏着原文件名(落盘名是随机的,链接里看不出是哪张图)
if (strlen($out) > 2) {
$nl = unpack('n', substr($out, 0, 2))[1];
if ($nl > 0 && $nl <= 255 && strlen($out) > 2 + $nl) {
$origName = substr($out, 2, $nl);
$out = substr($out, 2 + $nl);
}
}
$data = $out;
}
header('X-Filename: ' . rawurlencode($origName));
header('Access-Control-Expose-Headers: X-Filename');
// 上传时已经 gzip 过,解密后就是 gzip 流,直接标 Content-Encoding 让浏览器解;
// 明文文件(如 dxf在线压一次DXF 文本能压到 1/8 左右
$send = $data;
if (substr($send, 0, 2) === "\x1f\x8b") {
header('Content-Encoding: gzip');
} elseif (!ini_get('zlib.output_compression') && strlen($data) > 1024) {
$gz = gzencode($data, 6);
if ($gz !== false) { $send = $gz; header('Content-Encoding: gzip'); }
}
header('Content-Type: application/octet-stream');
header('Content-Length: ' . strlen($send));
header('Cache-Control: no-store');
header('Content-Disposition: inline');
echo $send;

View File

@@ -0,0 +1,38 @@
<?php
/**
* DWG/DXF 查看器 - 令牌接口
* 同源校验 + HMAC 短时令牌5 分钟)。与 STEPViewer 的 api/token.php 同一套设计。
* 部署前务必改掉 SECRET。
*/
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$SECRET = 'be323d1b90bd2fda624083d4d1d716b7';
$TTL = 300;
function json_out($code, $obj) {
http_response_code($code);
echo json_encode($obj, JSON_UNESCAPED_UNICODE);
exit;
}
$f = $_GET['f'] ?? '';
if (!$f || !preg_match('/\.(dwg|dxf|enc)$/i', $f)) json_out(400, ['error' => 'bad file']);
if (strpos($f, '..') !== false) json_out(400, ['error' => 'bad path']);
// 只允许本站页面来取令牌,挡掉直接爬取。
// 优先看 Sec-Fetch-Site由查看器页面发起的同源 fetch 恒为 same-origin跨站 iframe
// 嵌入也不受影响,且不会被父站点的 Referrer-Policy 剥掉);没有该头的旧浏览器
// 退回「Referer 含本站 host」的校验。直链/curl 两种都过不了。
$fetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? '';
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
if ($fetchSite !== '') {
if ($fetchSite !== 'same-origin' && $fetchSite !== 'same-site') json_out(403, ['error' => 'forbidden']);
} elseif (!$host || strpos($referer, $host) === false) {
json_out(403, ['error' => 'forbidden']);
}
$exp = time() + $TTL;
$sig = substr(hash_hmac('sha256', $f . '|' . $exp, $SECRET), 0, 16);
json_out(200, ['token' => $sig, 'expires' => $exp]);

View File

@@ -0,0 +1,109 @@
<?php
/**
* DWG/DXF 查看器 - 上传接口
* 接收原始字节流 + X-Filename 头gzip 后异或加密存成 uploads/xxx.enc。
* 落盘就是密文,即使目录被翻到也拿不到可用的图纸。
* SECRET 必须与 token.php、model.php 一致。
*/
header('Content-Type: application/json; charset=utf-8');
$SECRET = 'be323d1b90bd2fda624083d4d1d716b7';
$MAX_SIZE = 200 * 1024 * 1024;
// 私有目录Web 目录外的绝对路径图纸只存这里web 目录里根本没有图纸文件可下载。
// 群晖 File Station 在 web 目录外建 /volume1/dwgviewer_private 并给 http 用户组读写权限;
// 路径不同就改成你的实际路径。
$PRIVATE_DIR = '/volume1/dwgviewer_private';
function json_out($code, $obj) {
http_response_code($code);
echo json_encode($obj, JSON_UNESCAPED_UNICODE);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') json_out(405, ['error' => 'method not allowed']);
// 只允许本站页面发起上传(与 token.php 同一策略Sec-Fetch-Site 优先、Referer 兜底,
// 父站点的 Referrer-Policy 剥不掉 Sec-Fetch-Site跨站 iframe 嵌入也能上传)
$fetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? '';
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
if ($fetchSite !== '') {
if ($fetchSite !== 'same-origin' && $fetchSite !== 'same-site') json_out(403, ['error' => 'forbidden']);
} elseif (!$host || strpos($referer, $host) === false) {
json_out(403, ['error' => 'forbidden']);
}
$len = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
if ($len <= 0) json_out(400, ['error' => 'empty body']);
if ($len > $MAX_SIZE) json_out(413, ['error' => 'file too large (max 200MB)']);
$raw = urldecode($_SERVER['HTTP_X_FILENAME'] ?? 'drawing.dwg');
$base = basename($raw);
$safe = preg_replace('/[^\w.\-\x{4e00}-\x{9fff}]/u', '_', $base);
if (!$safe || !preg_match('/\.(dwg|dxf)$/i', $safe)) json_out(400, ['error' => '只支持 .dwg / .dxf']);
if (strlen($safe) > 200) $safe = substr($safe, 0, 200);
// 落盘名是纯随机的,不带原文件名也不带真实后缀:
// 嵌入链接里就看不出这是哪张图、属于哪个项目,也没法按名字猜其它图纸。
// 原文件名放进加密体里,读取时由 model.php 通过 X-Filename 头带回来显示。
$name = bin2hex(random_bytes(12)) . '.enc';
$data = file_get_contents('php://input');
if ($data === false || strlen($data) !== $len) json_out(400, ['error' => 'incomplete upload']);
// 存储层压缩DWG 本身已压过DXF 文本能压到 1/8
$rawSize = strlen($data);
$data = gzencode($data, 6);
if ($data === false) json_out(500, ['error' => 'gzip failed']);
// 加密体 = 2 字节名字长度 + 原文件名 + gzip 数据
$data = pack('n', strlen($safe)) . $safe . $data;
$dir = $PRIVATE_DIR !== '' ? $PRIVATE_DIR : (__DIR__ . '/../uploads');
if (!is_dir($dir)) @mkdir($dir, 0755, true);
if (!is_dir($dir)) json_out(500, ['error' => 'upload dir not writable']);
$nonce = random_bytes(16);
$key = hash('sha256', $SECRET . $nonce, true);
$enc = '';
for ($i = 0; $i < strlen($data); $i++) {
$enc .= $data[$i] ^ $key[$i % strlen($key)];
}
if (file_put_contents($dir . '/' . $name, $nonce . $enc) === false) {
json_out(500, ['error' => 'write failed']);
}
// 私有目录模式下返回裸文件名:嵌入链接就是 ?file=xxx.enc读取端按 basename 在私有目录找
$entryPath = ($PRIVATE_DIR !== '' ? '' : 'uploads/') . $name;
// 记入首页数模库(入口目录的 library.json2D/3D 共享);库写失败不影响上传本身
try {
add_to_library($entryPath, $safe, '2d');
} catch (Throwable $e) { /* 忽略 */ }
json_out(200, ['path' => $entryPath, 'name' => $safe, 'size' => $rawSize]);
/**
* 把刚上传的文件追加进首页数模库(../../library.json
* 同路径已有记录就刷新名称和时间,否则插到最前。
*/
function add_to_library($path, $name, $type) {
$libFile = dirname(__DIR__, 2) . '/library.json';
$records = [];
if (is_file($libFile)) {
$old = json_decode((string)file_get_contents($libFile), true);
if (is_array($old) && isset($old['records']) && is_array($old['records'])) $records = $old['records'];
}
$found = false;
foreach ($records as $i => $rec) {
if (isset($rec['path']) && $rec['path'] === $path) {
$records[$i]['name'] = $name;
$records[$i]['time'] = time();
$found = true;
break;
}
}
if (!$found) {
array_unshift($records, ['path' => $path, 'name' => $name, 'type' => $type, 'note' => '', 'time' => time()]);
}
@file_put_contents($libFile, json_encode(['records' => $records], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
}

893
dev/DWGViewer/app.js Normal file
View File

@@ -0,0 +1,893 @@
/**
* DWG / DXF 图纸查看器 —— 界面与交互装配。
*
* 数据流:文件 → source.js(解析) → flatten.js(展平成显示列表) → render.js(画)
* 这里只负责把 UI 事件接到上面三层,以及图层面板、测量/批注工具、导出等外围功能。
*/
import ACI from './libs/dxf-parser/AutoCadColorIndex.js'
import { flatten } from './js/flatten.js'
import { BACKGROUNDS, Renderer } from './js/render.js'
import { extOf, loadDrawing } from './js/source.js'
import { ANNO_MODES, AnnoTool, MEASURE_MODES, MeasureTool, Snapper } from './js/tools.js'
const $ = (id) => document.getElementById(id)
const params = new URLSearchParams(location.search)
// ---------------------------------------------------------------- 状态
const S = {
doc: null,
shapes: [],
layerStats: new Map(), // 图层名 → 图元数
tool: 'select', // select | measure | anno | zoomBox
embed: params.get('embed') === '1',
}
const wrap = $('canvasWrap')
const renderer = new Renderer(wrap)
const snapper = new Snapper(renderer)
const measure = new MeasureTool(renderer, snapper, () => { renderStats(); renderer.requestDraw() })
const anno = new AnnoTool(renderer, () => renderer.requestDraw())
renderer.overlay = (ctx, r) => {
anno.draw(ctx, r)
if (S.tool === 'measure' || measure.results.length) measure.draw(ctx, r)
drawRubber(ctx, r)
}
if (S.embed) document.body.classList.add('embed')
// ---------------------------------------------------------------- 加载
function setLoading(on, text) {
$('loading').classList.toggle('hidden', !on)
if (text) $('loadingText').textContent = text
}
function showError(msg) {
let box = $('errBox')
if (!box) {
box = document.createElement('div')
box.id = 'errBox'
$('viewport').appendChild(box)
}
box.innerHTML = ''
box.append(msg)
const b = document.createElement('button')
b.className = 'small-btn'
b.textContent = '关闭'
b.style.marginLeft = '12px'
b.onclick = () => box.remove()
box.appendChild(b)
}
async function openBuffer(name, buffer) {
setLoading(true, '正在解析图纸...')
const t0 = performance.now()
try {
const doc = await loadDrawing(name, buffer, (m) => setLoading(true, m))
setLoading(true, '正在生成图形...')
await nextFrame()
const { shapes, bbox, stats } = flatten(doc)
if (!shapes.length) throw new Error('图纸里没有可显示的图元(可能是空图或全部实体类型暂不支持)')
S.doc = doc
S.buffer = buffer // 留着给「上传并生成嵌入链接」用
applyShapes(doc, shapes, bbox)
measure.clear()
anno.clear()
setUnit(unitLabel(doc), true)
buildLayoutSelect(doc)
$('fileName').textContent = `${name} · ${doc.format} · ${stats.entities} 实体 / ${shapes.length} 图元`
renderInfo(stats, performance.now() - t0)
setStatus(`已加载 ${name}`)
$('modelInfo').textContent = `${shapes.length} 图元 · ${(performance.now() - t0).toFixed(0)}ms`
} catch (e) {
console.error(e)
showError(`打开失败:${e && e.message ? e.message : e}`)
} finally {
setLoading(false)
}
}
const nextFrame = () => new Promise((r) => requestAnimationFrame(() => r()))
/** 把一份显示列表装进渲染器并刷新左侧图层面板 */
function applyShapes(doc, shapes, bbox) {
S.shapes = shapes
S.layerStats = new Map()
for (const s of shapes) S.layerStats.set(s.layer, (S.layerStats.get(s.layer) || 0) + 1)
renderer.setDocument(doc, shapes, bbox)
renderer.zoomExtents()
isolated = null
buildLayerPanel()
renderer.draw()
}
/**
* 模型空间 / 图纸空间布局切换。
* 国内机械图多半把图框画在模型空间,所以只有布局里真有实体时才显示这个下拉。
*/
function buildLayoutSelect(doc) {
const sel = $('layoutSelect')
const used = (doc.layouts || []).filter((l) => l.entities && l.entities.length)
sel.innerHTML = ''
const add = (val, text) => { const o = document.createElement('option'); o.value = val; o.textContent = text; sel.appendChild(o) }
add('model', '模型空间')
used.forEach((l, i) => add(String(i), l.name.replace(/^\*/, '') || `布局 ${i + 1}`))
$('layoutBox').classList.toggle('hidden', used.length === 0)
sel.value = 'model'
sel.onchange = () => {
const ents = sel.value === 'model' ? doc.modelEntities : used[+sel.value].entities
const { shapes, bbox } = flatten(doc, { entities: ents })
if (!shapes.length) { showError('该布局里没有可显示的图元'); sel.value = 'model'; return }
applyShapes(doc, shapes, bbox)
$('modelInfo').textContent = `${shapes.length} 图元`
}
}
/** 单位DWG 的 $INSUNITS 常年是默认值,所以给用户留了手动覆盖 */
function setUnit(u, fromFile) {
measure.unit = u
$('optUnit').value = u
$('optUnitSrc').textContent = fromFile ? '自动' : '手动'
measure.refresh()
renderStats()
renderer.requestDraw()
}
function openFile(file) {
const ext = extOf(file.name)
if (ext !== 'dwg' && ext !== 'dxf') { showError('只支持 .dwg 和 .dxf 文件'); return }
S.loadPath = null // 本地文件没有服务器路径,嵌入前要先上传
const fr = new FileReader()
fr.onload = () => openBuffer(file.name, fr.result)
fr.onerror = () => showError('文件读取失败')
setLoading(true, '正在读取文件...')
fr.readAsArrayBuffer(file)
}
async function openUrl(url, loadPath) {
setLoading(true, '正在读取图纸...')
try {
const { buffer, name } = await fetchDrawing(url)
S.loadPath = loadPath || url // 嵌入链接沿用真正打开的路径,别用裸文件名
await openBuffer(name || url.split('/').pop() || url, buffer)
} catch (e) {
setLoading(false)
showError(`读取失败:${e.message}${url}`)
}
}
/**
* 取图纸字节。
*
* 站内路径优先走「令牌 → 解密接口」:服务端用 .htaccess 封掉了 .dwg/.dxf/.enc 的直接 GET
* 只有拿到 5 分钟有效的 HMAC 令牌才能通过 api/model.php 读到内容,挡住直链下载和爬取。
* 接口不存在时(比如本地 python 开发服务器没有 PHP自动退回直连不影响开发。
*/
async function fetchDrawing(url) {
if (!/^https?:\/\//i.test(url)) {
const via = await tryTokenFetch(url)
if (via) return via
}
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return { buffer: await res.arrayBuffer(), name: '' }
}
/**
* 走令牌接口取图纸;接口没部署就返回 null 让调用方直连。
*
* 判断「接口有没有部署」看的是响应内容而不是异常文本:本地开发用的 python
* 静态服务器不会执行 PHP会把 token.php 的源码当普通文件 200 发回来,
* 只看状态码会误判成接口可用,然后 json() 直接炸掉。
*/
async function tryTokenFetch(url) {
let tok
try {
const tr = await fetch(`api/token.php?f=${encodeURIComponent(url)}`, { cache: 'no-store' })
if (!tr.ok) return null
if (!/application\/json/i.test(tr.headers.get('content-type') || '')) return null
tok = await tr.json()
} catch {
return null // 网络层失败:当作没部署
}
if (!tok || !tok.token) return null
// 到这里说明接口确实在跑,它再拒绝就该明确报错,不能悄悄退回直连
const mr = await fetch(
`api/model.php?f=${encodeURIComponent(url)}&t=${tok.token}&e=${tok.expires}`, { cache: 'no-store' })
if (!mr.ok) {
const j = await mr.json().catch(() => ({}))
throw new Error(j.error || `读取接口返回 ${mr.status}`)
}
// 落盘名是随机的,真实文件名由接口通过头带回
const hdr = mr.headers.get('X-Filename')
return { buffer: await mr.arrayBuffer(), name: hdr ? decodeURIComponent(hdr) : '' }
}
/** 把当前图纸上传到服务器(加密落盘 + 随机文件名),返回可用于嵌入的路径 */
async function uploadCurrent() {
if (!S.buffer) throw new Error('还没有打开图纸')
const res = await fetch('api/upload.php', {
method: 'POST',
headers: { 'X-Filename': encodeURIComponent(S.doc.name), 'Content-Type': 'application/octet-stream' },
body: S.buffer,
})
const j = await res.json().catch(() => ({}))
if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`)
return j.path
}
/**
* $INSUNITS → 单位标签。
* 只认公制的显式取值0未设和 1英寸多半是 CAD 的默认值没改过,
* 国内机械图实际都是毫米,直接按英寸显示会差 25.4 倍。用户可在设置里改。
*/
function unitLabel(doc) {
const u = doc.header && doc.header.INSUNITS
return ({ 2: 'ft', 4: 'mm', 5: 'cm', 6: 'm', 13: 'µm' })[u] || 'mm'
}
// ---------------------------------------------------------------- 图层面板
function buildLayerPanel() {
const box = $('tree')
box.innerHTML = ''
if (!S.doc) return
const names = [...S.doc.layers.keys()].sort((a, b) => a.localeCompare(b, 'zh'))
// 没有任何图元的图层排到后面
names.sort((a, b) => (S.layerStats.get(b) || 0) - (S.layerStats.get(a) || 0))
$('layerCount').textContent = `(${names.length})`
for (const name of names) {
const layer = S.doc.layers.get(name)
const n = S.layerStats.get(name) || 0
const row = document.createElement('div')
row.className = 'tree-row'
row.dataset.layer = name
const cb = document.createElement('input')
cb.type = 'checkbox'
cb.className = 'tree-check'
cb.checked = !renderer.hidden.has(name)
cb.onchange = () => {
if (cb.checked) renderer.hidden.delete(name); else renderer.hidden.add(name)
row.classList.toggle('off', !cb.checked)
renderer.requestDraw()
}
const sw = document.createElement('span')
sw.className = 'layer-swatch'
sw.style.background = swatchColor(layer)
const lb = document.createElement('span')
lb.className = 'tree-label'
lb.textContent = name
lb.title = `${name}${n} 个图元)`
const num = document.createElement('span')
num.className = 'tree-num'
num.textContent = n || ''
row.append(cb, sw, lb, num)
// 点行(不含勾选框)= 只显示这一层
row.onclick = (e) => {
if (e.target === cb) return
isolateLayer(name)
}
box.appendChild(row)
}
if (!names.length) box.innerHTML = '<div class="tree-empty">没有图层</div>'
}
function swatchColor(layer) {
if (!layer) return '#000'
let c = 0
if (typeof layer.trueColor === 'number') c = layer.trueColor
else if (layer.colorIndex != null) {
const idx = Math.abs(layer.colorIndex)
c = idx === 7 ? 0x000000 : ACI_CACHE[idx] || 0x000000
}
return `#${(c >>> 0).toString(16).padStart(6, '0')}`
}
const ACI_CACHE = ACI
let isolated = null
function isolateLayer(name) {
if (isolated === name) {
renderer.hidden.clear()
isolated = null
} else {
renderer.hidden.clear()
for (const k of S.doc.layers.keys()) if (k !== name) renderer.hidden.add(k)
isolated = name
}
syncLayerChecks()
renderer.requestDraw()
setStatus(isolated ? `只显示图层「${name}」,再点一次恢复` : '已显示全部图层')
}
function syncLayerChecks() {
for (const row of $('tree').children) {
const cb = row.querySelector('input')
if (!cb) continue
cb.checked = !renderer.hidden.has(row.dataset.layer)
row.classList.toggle('off', !cb.checked)
}
}
$('treeSearch').addEventListener('input', () => {
const q = $('treeSearch').value.trim().toLowerCase()
for (const row of $('tree').children) {
const name = (row.dataset.layer || '').toLowerCase()
const hit = !q || name.includes(q)
row.style.display = hit ? '' : 'none'
row.classList.toggle('matched', !!q && hit)
}
})
$('btnLayerAll').onclick = () => { renderer.hidden.clear(); isolated = null; syncLayerChecks(); renderer.requestDraw() }
$('btnLayerNone').onclick = () => {
for (const k of S.doc ? S.doc.layers.keys() : []) renderer.hidden.add(k)
isolated = null; syncLayerChecks(); renderer.requestDraw()
}
$('btnLayerInvert').onclick = () => {
for (const k of S.doc ? S.doc.layers.keys() : []) {
if (renderer.hidden.has(k)) renderer.hidden.delete(k); else renderer.hidden.add(k)
}
isolated = null; syncLayerChecks(); renderer.requestDraw()
}
// ---------------------------------------------------------------- 工具栏
function setTool(t) {
S.tool = t
for (const b of document.querySelectorAll('#toolbar .tool-btn[data-tool]')) {
b.classList.toggle('active', b.dataset.tool === t)
}
const isMeasure = t === 'measure'
const isAnno = t === 'anno'
$('toolbar').classList.toggle('hidden', isMeasure || isAnno)
$('measurePanel').classList.toggle('hidden', !isMeasure)
$('annoPanel').classList.toggle('hidden', !isAnno)
if (!isMeasure) measure.setMode(null)
if (!isAnno) anno.setMode(null)
wrap.classList.toggle('crosshair', isMeasure || isAnno || t === 'zoomBox')
setStatus(TOOL_HINT[t] || '')
renderer.requestDraw()
}
const TOOL_HINT = {
select: '选择:点击图元查看属性 · 拖动平移 · 滚轮缩放 · 双击全图',
measure: '测量:先选测量方式,再在图上点取(自动捕捉端点/中点/圆心)',
anno: '批注:先选批注方式,在图上拖动绘制',
zoomBox: '框选放大:按住左键拖出矩形',
}
for (const b of document.querySelectorAll('#toolbar .tool-btn[data-tool]')) {
b.onclick = () => setTool(b.dataset.tool)
}
// 测量子面板
const mBox = $('measureModes')
for (const m of MEASURE_MODES) {
const b = document.createElement('button')
b.className = 'measure-btn'
b.title = m.name
b.innerHTML = `<img src="./icons/${m.icon}" alt=""><span>${m.name}</span>`
b.onclick = () => {
const on = measure.mode === m.id
measure.setMode(on ? null : m.id)
for (const x of mBox.children) x.classList.remove('active')
if (!on) b.classList.add('active')
setStatus(on ? '' : `${m.name}:在图上点取`)
}
mBox.appendChild(b)
}
$('btnMeasureClose').onclick = () => setTool('select')
$('btnMeasureClear').onclick = () => measure.clear()
$('btnStats').onclick = () => togglePanel('stats')
// 批注子面板
const aBox = $('annoModes')
for (const m of ANNO_MODES) {
const b = document.createElement('button')
b.className = 'measure-btn'
b.title = m.name
b.innerHTML = `<img src="./icons/${m.icon}" alt=""><span>${m.name}</span>`
b.onclick = () => {
const on = anno.mode === m.id
anno.setMode(on ? null : m.id)
for (const x of aBox.children) x.classList.remove('active')
if (!on) b.classList.add('active')
}
aBox.appendChild(b)
}
$('btnAnnoClose').onclick = () => setTool('select')
$('btnAnnoClear').onclick = () => anno.clear()
$('btnAnnoUndo').onclick = () => anno.undo()
$('annoColor').oninput = (e) => { anno.color = e.target.value }
$('annoWidth').onchange = (e) => { anno.width = +e.target.value }
// 其它工具按钮
$('btnLayerPanel').onclick = (e) => {
if (e) e.stopPropagation() // 别让「点击外部收起抽屉」把这次打开又关掉
if (window.innerWidth <= 768) {
// 手机:图层面板是抽屉,点按钮开合(默认隐藏)
document.body.classList.toggle('tree-open')
} else {
// 桌面:默认显示,点击隐藏/显示侧栏
document.body.classList.toggle('no-tree')
}
renderer.resize()
}
// 手机抽屉:✕ 按钮关闭 + 点击抽屉外部自动收起
const btnTreeClose = $('btnTreeClose')
if (btnTreeClose) btnTreeClose.onclick = () => document.body.classList.remove('tree-open')
document.addEventListener('click', (e) => {
if (window.innerWidth > 768 || !document.body.classList.contains('tree-open')) return
if (!$('treePanel').contains(e.target)) document.body.classList.remove('tree-open')
})
$('btnFind').onclick = () => togglePanel('find')
$('btnSettings').onclick = () => togglePanel('settings')
$('btnInfo').onclick = () => togglePanel('info')
$('btnReset').onclick = () => { setRotation(0); renderer.zoomExtents(); renderer.requestDraw() }
$('btnLineWeight').onclick = () => {
renderer.opts.lineWeight = !renderer.opts.lineWeight
$('btnLineWeight').classList.toggle('active', renderer.opts.lineWeight)
$('optLineWeight').checked = renderer.opts.lineWeight
renderer.requestDraw()
}
$('btnSnapshot').onclick = snapshot
$('btnPrint').onclick = printDrawing
$('btnOpen').onclick = () => $('fileInput').click()
$('fileInput').onchange = (e) => { if (e.target.files[0]) openFile(e.target.files[0]) }
$('btnFullscreen').onclick = () => toggleFullscreen()
$('btnEmbed').onclick = () => { togglePanel('embed'); updateEmbed() }
for (const el of document.querySelectorAll('.panel-close')) {
el.onclick = () => $(`${el.dataset.panel}Panel`).classList.add('hidden')
}
function togglePanel(name) {
const p = $(`${name}Panel`)
const willShow = p.classList.contains('hidden')
for (const n of ['stats', 'find', 'settings', 'info', 'embed']) {
if (n !== name) $(`${n}Panel`).classList.add('hidden')
}
p.classList.toggle('hidden', !willShow)
if (willShow && name === 'find') $('findInput').focus()
}
// ---------------------------------------------------------------- 显示设置
// ---- 背景(浅灰 / 米白 / 黑,与新迪 2D 一致)----
const bgBox = $('bgSwatches')
for (const b of BACKGROUNDS) {
const el = document.createElement('button')
el.className = 'bg-swatch'
el.dataset.bg = b.id
el.title = b.name
el.style.background = b.color
el.onclick = () => setBackground(b.id)
bgBox.appendChild(el)
}
/** 背景偏好存本地,下次打开还是上次那个底 */
function setBackground(id) {
const b = BACKGROUNDS.find((x) => x.id === id) || BACKGROUNDS[0]
renderer.opts.bg = b.color
for (const el of bgBox.children) el.classList.toggle('active', el.dataset.bg === b.id)
$('btnBg').classList.toggle('active', renderer.isDarkBg)
try { localStorage.setItem('dwgviewer.bg', b.id) } catch { /* 隐私模式下写不了,忽略 */ }
renderer.requestDraw()
}
/** 工具栏那个按钮循环切三种底 */
$('btnBg').onclick = () => {
const i = BACKGROUNDS.findIndex((b) => b.color === renderer.opts.bg)
const next = BACKGROUNDS[(i + 1) % BACKGROUNDS.length]
setBackground(next.id)
setStatus(`背景:${next.name}`)
}
$('optContrast').onchange = (e) => { renderer.opts.autoContrast = e.target.checked; renderer.requestDraw() }
// ---- 视图旋转 ----
function setRotation(rad, fromSlider) {
renderer.setRotation(rad)
const deg = Math.round((renderer.view.rot * 180) / Math.PI)
if (!fromSlider) $('optRot').value = deg
$('optRotVal').textContent = `${deg}°`
renderer.requestDraw()
}
/** 转 90°原本铺满视口的话转完继续铺满已经放大过就保持倍数不动 */
function rotate90(dir) {
const wasFitted = renderer.isFitted()
setRotation(renderer.view.rot + dir * Math.PI / 2)
if (wasFitted) renderer.zoomExtents()
renderer.requestDraw()
}
$('btnRotL2').onclick = () => rotate90(1)
$('btnRotR2').onclick = () => rotate90(-1)
$('btnRot0').onclick = () => setRotation(0)
$('optRot').oninput = (e) => setRotation((+e.target.value * Math.PI) / 180, true)
// ---- 全屏(底部工具栏那个在嵌入模式下也能点到)----
function toggleFullscreen() {
if (document.fullscreenElement) document.exitFullscreen()
else document.documentElement.requestFullscreen()
}
$('btnFull').onclick = toggleFullscreen
$('optText').onchange = (e) => { renderer.opts.showText = e.target.checked; renderer.requestDraw() }
$('optHatch').onchange = (e) => { renderer.opts.showHatch = e.target.checked; renderer.requestDraw() }
$('optLineWeight').onchange = (e) => {
renderer.opts.lineWeight = e.target.checked
$('btnLineWeight').classList.toggle('active', e.target.checked)
renderer.requestDraw()
}
$('optLwScale').oninput = (e) => {
renderer.opts.lineWeightScale = e.target.value / 100
$('optLwVal').textContent = `${(e.target.value / 100).toFixed(1)}x`
if (renderer.opts.lineWeight) renderer.requestDraw()
}
$('optUnit').onchange = (e) => setUnit(e.target.value, false)
$('optPrec').oninput = (e) => {
measure.precision = +e.target.value
$('optPrecVal').textContent = e.target.value
measure.refresh()
renderStats()
renderer.requestDraw()
}
// ---------------------------------------------------------------- 鼠标交互
let drag = null
let rubber = null // 框选/框放大的临时矩形(屏幕 CSS 像素)
wrap.addEventListener('contextmenu', (e) => e.preventDefault())
wrap.addEventListener('pointerdown', (e) => {
wrap.setPointerCapture(e.pointerId)
const pos = local(e)
// 中键/右键始终是平移;左键看当前工具
if (e.button === 1 || e.button === 2) {
drag = { mode: 'pan', x: e.clientX, y: e.clientY }
wrap.classList.add('panning')
return
}
if (e.button !== 0) return
if (S.tool === 'zoomBox') { rubber = { x0: pos.x, y0: pos.y, x1: pos.x, y1: pos.y, kind: 'zoom' }; return }
if (S.tool === 'anno' && anno.mode) { if (anno.down(pos.x, pos.y)) { drag = { mode: 'anno' }; return } }
if (S.tool === 'measure' && measure.mode) return // 测量在 pointerup 里按点击处理
drag = { mode: 'maybe-pan', x: e.clientX, y: e.clientY, sx: pos.x, sy: pos.y, moved: false }
})
wrap.addEventListener('pointermove', (e) => {
const pos = local(e)
updateCoord(pos)
if (rubber) { rubber.x1 = pos.x; rubber.y1 = pos.y; renderer.requestDraw(); return }
if (drag && drag.mode === 'anno') { anno.move(pos.x, pos.y); renderer.requestDraw(); return }
if (drag && (drag.mode === 'pan' || drag.mode === 'maybe-pan')) {
const dx = e.clientX - drag.x, dy = e.clientY - drag.y
if (drag.mode === 'maybe-pan' && Math.hypot(dx, dy) < 3) return
if (drag.mode === 'maybe-pan') { drag.mode = 'pan'; drag.moved = true; wrap.classList.add('panning') }
renderer.panByPixels(dx, dy)
drag.x = e.clientX; drag.y = e.clientY
renderer.requestDraw()
return
}
if (S.tool === 'measure' && measure.mode) { measure.hover(pos.x, pos.y); renderer.requestDraw(); return }
if (S.tool === 'select') {
const idx = renderer.pick(pos.x, pos.y, 6)
if (idx !== renderer.highlight) { renderer.highlight = idx; renderer.requestDraw() }
}
})
wrap.addEventListener('pointerup', (e) => {
const pos = local(e)
wrap.classList.remove('panning')
try { wrap.releasePointerCapture(e.pointerId) } catch { /* 指针已释放 */ }
if (rubber) {
const r = rubber; rubber = null
if (Math.abs(r.x1 - r.x0) > 6 && Math.abs(r.y1 - r.y0) > 6) {
const a = renderer.toWorld(r.x0, r.y0), b = renderer.toWorld(r.x1, r.y1)
renderer.zoomToBox([Math.min(a.x, b.x), Math.min(a.y, b.y), Math.max(a.x, b.x), Math.max(a.y, b.y)])
setTool('select')
}
renderer.requestDraw()
return
}
if (drag && drag.mode === 'anno') { anno.up(); drag = null; return }
const wasPan = drag && drag.mode === 'pan'
const wasClick = drag && drag.mode === 'maybe-pan'
drag = null
if (wasPan) return
if (e.button !== 0) return
if (S.tool === 'measure' && measure.mode) { measure.click(pos.x, pos.y); renderer.requestDraw(); return }
if (S.tool === 'select' && wasClick) selectAt(pos)
})
wrap.addEventListener('dblclick', () => {
if (S.tool === 'measure' && measure.mode) { measure.finish(); return }
renderer.zoomExtents()
renderer.requestDraw()
})
wrap.addEventListener('wheel', (e) => {
e.preventDefault()
const pos = local(e)
const f = e.deltaY < 0 ? 1.18 : 1 / 1.18
renderer.zoomAt(pos.x, pos.y, f)
renderer.requestDraw()
updateCoord(pos)
}, { passive: false })
function local(e) {
const r = wrap.getBoundingClientRect()
return { x: e.clientX - r.left, y: e.clientY - r.top }
}
function updateCoord(pos) {
const w = renderer.toWorld(pos.x, pos.y)
$('coordText').textContent = S.doc ? `X ${w.x.toFixed(2)} Y ${w.y.toFixed(2)} 1:${(1 / renderer.view.scale * renderer.dpr).toFixed(2)}` : ''
}
function selectAt(pos) {
const idx = renderer.pick(pos.x, pos.y, 6)
renderer.selection.clear()
if (idx != null) {
renderer.selection.add(idx)
showProps(renderer.shapes[idx])
} else {
$('propPanel').classList.add('hidden')
}
renderer.requestDraw()
}
function drawRubber(ctx, r) {
if (!rubber) return
const d = r.dpr
ctx.save()
// 框选矩形是屏幕上的物件,不跟着图纸转,所以撤掉 overlay 的视图旋转
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.strokeStyle = '#2b9ae8'
ctx.fillStyle = 'rgba(43,154,232,.12)'
ctx.lineWidth = 1.5 * d
ctx.setLineDash([5 * d, 4 * d])
const x = Math.min(rubber.x0, rubber.x1) * d, y = Math.min(rubber.y0, rubber.y1) * d
const w = Math.abs(rubber.x1 - rubber.x0) * d, h = Math.abs(rubber.y1 - rubber.y0) * d
ctx.fillRect(x, y, w, h)
ctx.strokeRect(x, y, w, h)
ctx.restore()
}
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
if (e.key === 'Escape') {
measure.cancel(); anno.setMode(anno.mode); rubber = null
renderer.selection.clear(); $('propPanel').classList.add('hidden')
renderer.requestDraw()
} else if (e.key === 'Enter') {
measure.finish(); renderer.requestDraw()
} else if (e.key === 'e' || e.key === 'E') {
renderer.zoomExtents(); renderer.requestDraw()
}
})
// ---------------------------------------------------------------- 面板内容
function renderStats() {
const box = $('statsList')
box.innerHTML = ''
if (!measure.results.length) { box.innerHTML = '<div class="stats-empty">暂无测量结果</div>'; return }
measure.results.forEach((r, i) => {
const row = document.createElement('div')
row.className = 'stats-row'
row.innerHTML = `<span class="stats-type">${r.type}</span><span class="stats-value" title="${r.text}">${r.text}</span>`
const del = document.createElement('span')
del.className = 'stats-del'
del.textContent = '×'
del.onclick = () => measure.remove(i)
row.appendChild(del)
box.appendChild(row)
})
}
function renderInfo(stats, ms) {
const d = S.doc
const b = renderer.bbox
const kv = (k, v) => `<div class="kv"><b>${k}</b><span>${v}</span></div>`
const types = Object.entries(stats.byType).sort((a, b2) => b2[1] - a[1])
.map(([k, v]) => `${k} ${v}`).join('、')
$('infoBody').innerHTML =
kv('文件', d.name) +
kv('格式', d.format) +
kv('图层', d.layers.size) +
kv('块', d.blocks.size) +
kv('实体', stats.entities) +
kv('图元', S.shapes.length) +
kv('范围', `${(b[2] - b[0]).toFixed(1)} × ${(b[3] - b[1]).toFixed(1)} ${measure.unit}`) +
kv('解析耗时', `${ms.toFixed(0)} ms`) +
kv('实体分布', types || '—') +
`<div class="lic">解析内核:${d.format === 'DWG' ? 'LibreDWG (WebAssembly)' : 'dxf-parser'} 许可证 ${d.license}
${d.format === 'DWG' ? 'GPL-3.0 会传染到分发的前端代码,商用前请评估,或把 DWG 解析换成服务端/商业 SDK。' : ''}</div>`
}
function showProps(s) {
if (!s) return
const e = s.ent || {}
const kv = (k, v) => (v == null || v === '' ? '' : `<div class="kv"><b>${k}</b><span>${v}</span></div>`)
const b = s.bbox
$('propBody').innerHTML =
kv('类型', e.type || s.kind) +
kv('图层', s.layer) +
kv('句柄', e.handle) +
kv('颜色', `#${(s.color >>> 0).toString(16).padStart(6, '0')}`) +
kv('线型', s.lt || 'Continuous') +
kv('线宽', s.lw >= 0 ? `${(s.lw / 100).toFixed(2)} mm` : '随层') +
kv('文字', s.plain ? s.plain.slice(0, 200) : '') +
kv('半径', s.kind === 'arc' ? s.rx.toFixed(3) : '') +
kv('包围盒', `${(b[2] - b[0]).toFixed(2)} × ${(b[3] - b[1]).toFixed(2)}`)
$('propPanel').classList.remove('hidden')
}
// ---------------------------------------------------------------- 文字查找
$('findInput').addEventListener('keydown', (e) => { if (e.key === 'Enter') doFind() })
function doFind() {
const q = $('findInput').value.trim().toLowerCase()
const box = $('findResult')
box.innerHTML = ''
if (!q) { box.innerHTML = '<div class="stats-empty">输入关键字后回车</div>'; return }
const hits = []
for (const s of S.shapes) {
if (!s.plain) continue
if (s.plain.toLowerCase().includes(q)) hits.push(s)
if (hits.length >= 300) break
}
if (!hits.length) { box.innerHTML = '<div class="stats-empty">没有匹配的文字</div>'; return }
for (const s of hits) {
const row = document.createElement('div')
row.className = 'find-row'
row.innerHTML = `${escapeHtml(s.plain.slice(0, 60))}<span class="find-layer">${escapeHtml(s.layer)}</span>`
row.onclick = () => {
const b = s.bbox
const pad = Math.max((b[2] - b[0]), (b[3] - b[1])) * 6 + 1
renderer.zoomToBox([b[0] - pad, b[1] - pad, b[2] + pad, b[3] + pad])
renderer.selection.clear()
renderer.selection.add(s.i)
renderer.requestDraw()
}
box.appendChild(row)
}
setStatus(`找到 ${hits.length} 处文字`)
}
const escapeHtml = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]))
// ---------------------------------------------------------------- 导出
function snapshot() {
renderer.draw()
renderer.canvas.toBlob((blob) => {
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = `${(S.doc && S.doc.name || 'drawing').replace(/\.[^.]+$/, '')}.png`
a.click()
setTimeout(() => URL.revokeObjectURL(a.href), 5000)
}, 'image/png')
}
function printDrawing() {
if (!S.doc) return
renderer.draw()
const url = renderer.canvas.toDataURL('image/png')
const w = window.open('', '_blank')
if (!w) { showError('浏览器拦截了打印窗口,请允许弹出窗口'); return }
w.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${escapeHtml(S.doc.name)}</title>
<style>@page{size:auto;margin:8mm}body{margin:0}img{width:100%}</style></head>
<body><img src="${url}" onload="window.focus();window.print();"></body></html>`)
w.document.close()
}
// ---------------------------------------------------------------- 嵌入
function updateEmbed() {
// 默认沿用图纸真正打开时的路径(相对查看器目录),避免生成打不开的裸文件名链接;
// 本地拖进来的图纸没有服务器路径,得先「上传当前图纸」。
// 用 ?file= 打开时把真实路径回填到输入框(与 STEPViewer 行为一致),
// 否则「图纸路径」框空着、只有地址生成,容易误以为坏了。
if (S.loadPath && !$('embedPath').value.trim()) $('embedPath').value = S.loadPath
const path = $('embedPath').value.trim()
const base = location.origin + location.pathname
if (!path) {
$('embedUrl').value = ''
$('embedCode').value = ''
return
}
const url = `${base}?file=${encodeURIComponent(path)}&embed=1`
$('embedUrl').value = url
$('embedCode').value = `<iframe src="${url}" width="100%" height="640" frameborder="0" allowfullscreen></iframe>`
}
$('embedPath').addEventListener('input', updateEmbed)
$('btnUpload').onclick = async () => {
const btn = $('btnUpload')
btn.disabled = true
btn.textContent = '上传中...'
try {
const path = await uploadCurrent()
if (!path) throw new Error('上传接口返回异常(接口未部署)')
$('embedPath').value = path
updateEmbed()
setStatus(`已加密上传:${path}`)
// 嵌在入口页里时通知它刷新首页数模库
try { window.parent.postMessage({ type: 'library-updated' }, '*') } catch { /* 独立打开 */ }
} catch (e) {
showError(`上传失败:${e.message}(本地开发服务器没有 PHP需要在正式站点上试`)
} finally {
btn.disabled = false
btn.textContent = '上传当前图纸'
}
}
$('btnCopyUrl').onclick = () => copy($('embedUrl').value)
$('btnCopyCode').onclick = () => copy($('embedCode').value)
function copy(t) {
navigator.clipboard.writeText(t).then(() => setStatus('已复制到剪贴板'), () => setStatus('复制失败,请手动选择'))
}
// ---------------------------------------------------------------- 杂项
function setStatus(t) { $('statusText').textContent = t || TOOL_HINT[S.tool] || '就绪' }
new ResizeObserver(() => renderer.resize()).observe(wrap)
// 拖拽打开
let dragDepth = 0
document.addEventListener('dragenter', (e) => { e.preventDefault(); dragDepth++; $('dropOverlay').classList.remove('hidden') })
document.addEventListener('dragleave', () => { if (--dragDepth <= 0) { dragDepth = 0; $('dropOverlay').classList.add('hidden') } })
document.addEventListener('dragover', (e) => e.preventDefault())
document.addEventListener('drop', (e) => {
e.preventDefault()
dragDepth = 0
$('dropOverlay').classList.add('hidden')
const f = e.dataTransfer.files[0]
if (f) openFile(f)
})
// 调试用:控制台里可以直接摸到渲染器、文档和工具
window.__R = renderer
window.__S = S
window.__ANNO = anno
window.__MEASURE = measure
// 恢复上次的背景偏好;嵌入时可用 ?bg=black|beige|grey 直接指定
let savedBg = BACKGROUNDS[0].id
try { savedBg = params.get('bg') || localStorage.getItem('dwgviewer.bg') || savedBg } catch { /* 隐私模式读不到 */ }
setBackground(savedBg === 'dark' ? 'black' : savedBg)
// 先画一次底色,否则 alpha:false 的画布初始是黑的
renderer.draw()
setStatus()
// 统一入口(../index.html在 iframe 里把用户选的图纸传进来
window.addEventListener('message', (e) => {
const data = e.data
if (!data || data.type !== 'viewer-file' || data.kind !== 'dwg') return
try { if (e.origin !== location.origin) return } catch { return }
openBuffer(data.name || 'drawing.dwg', data.buffer)
})
try { window.parent.postMessage({ type: 'viewer-ready' }, '*') } catch { /* 没嵌在入口里 */ }
// URL 参数直接打开
const fileParam = params.get('file')
if (fileParam) openUrl(fileParam, fileParam)

BIN
dev/DWGViewer/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
dev/DWGViewer/icons/sun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Some files were not shown because too many files have changed in this diff Show More