static 폴더 및 하위 파일 업로드

This commit is contained in:
sotam0316
2026-04-22 12:05:03 +09:00
commit 514b209a5a
203 changed files with 29494 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
from flask import Flask
from routes.main_routes import main_bp
from routes.api_routes import api_bp
def create_app():
app = Flask(__name__)
# Register Blueprints
app.register_blueprint(main_bp)
app.register_blueprint(api_bp)
return app
if __name__ == '__main__':
app = create_app()
# 로컬 실행 최적화
print("------------------------------------------")
print("drawNET Premium Server Starting...")
print("URL: http://127.0.0.1:5000")
print("------------------------------------------")
app.run(debug=True, port=5000)
+8
View File
@@ -0,0 +1,8 @@
{
"mousewheel": {
"factor": 1.05,
"minScale": 0.1,
"maxScale": 10.0
},
"log_level": "info"
}
+238
View File
@@ -0,0 +1,238 @@
# drawNET System Architecture: Native JSON-Object Topology (v1.0)
> **"Programs must be written for people to read, and only incidentally for machines to execute."** — *Harold Abelson*
> **"What is not documented, does not exist in the eyes of Engineering."** — *Software Engineering Proverb*
---
## 1. 개요 (Abstract)
drawNET은 단순한 다이어그램 그리기 도구를 넘어, 엔터프라이즈급 네트워크 토폴로지 설계 및 엔니지어링을 위한 **객체 지향적 시각화 프레임워크**입니다. 초기 DSL(Domain Specific Language) 기반의 텍스트 중심 구조에서 탈피하여, 고성능 그래프 엔진인 **AntV X6 v2**를 기반으로 한 **JSON-Native 아키텍처**로 진화했습니다. 본 시스템은 데이터의 영속성(Persistence), 정밀한 공간 쿼리(Spatial Query), 그리고 멀티레이어 논리 격리를 핵심 가치로 삼는 **v1.0 Premium Edition**입니다.
---
## 2. 설계 철학 (Design Philosophy)
### 2.1 Drawing에서 Engineering으로
기존 도구들이 시각적 표현에 집중했다면, drawNET은 **"데이터가 주도하는 시각화"**를 지향합니다. 모든 시각적 요소는 정규화된 데이터 모델의 하위 표현(View)에 불과하며, 핵심 로직은 UUID 기반의 객체 관계망에서 동작합니다.
### 2.2 하이브리드 인터페이스 (The Hybrid UI Strategy)
사용자에게는 직관적인 GUI(마우스 조작)를 제공하지만, 백그라운드에서는 모든 행위가 정형화된 JSON 스트림으로 변환되어 저장됩니다. 이는 향후 AI 기반의 토폴로지 자동 최적화 및 이상 탐지(Anomaly Detection)를 위한 학습 데이터셋 구축에 최적화된 구조입니다.
---
## 3. 핵심 아키텍처 모델 (Core Architecture Model)
drawNET은 **SSoT (Single Source of Truth)** 원칙에 따라 모든 상태를 단일 JSON 객체로 관리합니다.
### 3.1 논리 흐름 (Logical Workflow)
시스템의 데이터 처리는 다음과 같은 순환 구조를 따릅니다.
1. **Interaction Layer**: 사용자의 드래그, 단축키, 속성 변경을 수집합니다.
2. **Event Orchestration**: 객체 간 부모-자식 관계 변경(Embedding), 연결 상태 변경(Connecting) 이벤트를 감지하여 데이터 무결성을 검증합니다.
3. **Data Transformation**: 변경된 상태를 X6 셀(Cell)의 `data` 속성에 즉각 반영합니다.
4. **Persistence Layer**: 변경된 전체 그래프 상태를 `graph.toJSON()`으로 직렬화하여 영속 저장소(LocalStorage/Server)에 동기화합니다.
### 3.2 UUID 기반 객체 참조 (UUID-based Referencing)
모든 노드와 엣지는 시스템 전역에서 유일한 **UUID**를 식별자로 가집니다. 이는 **RFC4122 v4** 표준을 철저하게 준수하며, 라벨(Label)이나 명칭 변경 시에도 객체 간의 참조 무결성(Referential Integrity)이 깨지지 않도록 보장하며, 대규모 프로젝트에서 복잡한 계층 구조를 추적하는 핵심 메커니즘입니다.
### 3.3 시스템 구조도 (System Overview Diagram)
본 시스템의 모듈 간 상호작용과 데이터 흐름은 다음과 같습니다.
```mermaid
graph TD
User["사용자 (User)"]
X6["AntV X6 Graph Engine"]
Sidebar["속성 사이드바 (Sidebar)"]
Persistence["영속성 제어 (persistence.js)"]
State["Shared State (state.js)"]
LocalStorage["Local Storage / Server"]
User -- "UI 상호작용" --> X6
X6 -- "JSON Export" --> Persistence
Persistence -- "JSON Save" --> LocalStorage
LocalStorage -- "JSON Load" --> Persistence
Persistence -- "graph.fromJSON()" --> X6
X6 -- "객체 선택 (UUID)" --> Sidebar
Sidebar -- "데이터 수정 (setData)" --> X6
subgraph Frontend [Client Side]
X6
Sidebar
State
Persistence
end
```
---
## 4. 멀티레이어 논리 격리 및 제어 (Multi-Layer & Logical Isolation)
drawNET은 단순히 선을 겹쳐 그리는 도구가 아닌, 복잡한 인프라를 **논리적 도메인(Domain)**으로 분리하여 관리하는 차세대 아키텍처를 채택했습니다. 이는 기존의 드로잉 도구(Draw.io, Visio 등)와 가장 극명하게 대비되는 drawNET만의 독보적 강점입니다.
### 4.1 Visual Layer vs. Logical Engineering Layer
| 비교 항목 | 기존 도구 (Visio, Draw.io) | **drawNET Premium** |
| :--- | :--- | :--- |
| **핵심 성격** | **Visual Layer** (시각적 그룹화) | **Logical Layer** (엔지니어링 격리) |
| **주요 용도** | 단순 Show/Hide (투명 비닐지 방식) | 도메인별 논리 격리 및 데이터 제어 |
| **데이터 일관성** | 레이어 간 복사 시 별개 객체로 분리 | 동일 UUID 기반의 다중 관점(View) 동기화 |
| **상호 작용** | 레이어 간 논리적 연동 불가 | Cross-layer Tunneling (계층 간 연동 추적) |
| **활용 가치** | "그림의 정리 정돈" | "복잡한 시스템의 통합 거버넌스" |
### 4.2 왜 '논리적 격리'인가? (The Reason Why)
엔터프라이즈급 인프라 설계에서 물리(Physical), 논리(Logical), 보안(Security) 데이터를 하나의 화면에 그리는 것은 불가능에 가깝습니다. drawNET은 이를 해결하기 위해 다음과 같은 고난도 기술을 구현했습니다.
1. **도메인 격리(Domain Isolation)**: L1(케이블/랙), L2(VLAN/IP), L3(보안 정책) 등 각 도메인의 정밀한 분리를 통해 설계의 복잡도를 낮추고 각 담당자가 본인의 영역에 집중하게 합니다.
2. **Single Source of Truth (SSOT)**: 1번 레이어의 '코어 스위치'와 3번 레이어의 '보안 장비'가 동일한 물리 장비라면, drawNET은 단 하나의 데이터 소스를 공유하여 어느 레이어에서 수정해도 전체 정합성이 유지되게 설계되었습니다.
3. **지능형 터널링(Layer Tunneling)**: 레이어는 분리되어 있지만, 데이터 경로(Path)는 연결되어 있습니다. 물리적 케이블이 끊어졌을 때 논리 레이어의 패킷 흐름에 어떤 영향을 주는지 **교차 레이어 분석**이 가능합니다.
### 4.3 공간 쿼리 및 부모 선택 알고리즘 (Innermost Heuristic)
노드가 중첩된 그룹 영역으로 이동할 때, 시스템은 **Bounding Box 면적 기반의 Innermost 알고리즘**을 수행합니다.
- **알고리즘**: 드롭 포인트를 포함하는 모든 부모 후보 중 면적이 가장 작은(가장 구체적인) 그룹을 최종 부모로 선택합니다.
- **의도**: 이를 통해 복잡한 랙(Rack) 및 사이트(Site) 계층 내부로의 직관적인 객체 삽입이 가능해집니다.
---
## 5. 저장 방식의 지향성과 확장성 (Storage & Scalability)
### 5.1 JSON-Native의 이점
- **VCS 친화성**: 텍스트 기반 JSON 파일로 저장되므로, Git과 같은 버전 관리 시스템을 통해 시나리오별 변경 이력을 관리하기 용이합니다.
- **상호운용성 (Interoperability)**: Python, JS 등 다양한 언어에서 표준 JSON 라이브러리를 통해 프로젝트 파일에 접근하여 자동화된 분석 보고서를 생성할 수 있습니다.
### 5.2 에셋 라이브러리 확장성 (Modular & Open Asset Architecture)
drawNET의 에셋 시스템은 폐쇄적### 7. 전문 내보내기 서브시스템 및 하이브리드 리포팅 (High-Fidelity Export & Hybrid Reporting)
drawNET은 단순한 캔버스 캡처를 넘어, 엔지니어링 결과물의 시각적 완밀성과 데이터의 편집 가능성을 동시에 확보하는 **하이브리드 내보내기 전략**을 취합니다.
#### 7.1 하이브리드 이미지-객체 모델 (Hybrid Image-Object Model)
PPTX 리포트 생성 시, 시스템은 두 가지 서로 다른 렌더링 경로를 결합합니다.
1. **Topology View (High-Res Snapshot)**: 복잡한 선(Path) 라우팅, SVG 그림자, 반투명 그룹 등은 PPTX 네이티브 객체로 변환 시 시각적 손실이 큽니다. 이를 방지하기 위해 200% 스케일의 고해상도 PNG 스냅샷으로 캔버스를 캡처하여 삽입합니다.
2. **Inventory Data (Native PPTX Objects)**: 장비 목록, 프로젝트 정보, 요약 통계는 수정이 용이해야 합니다. 따라서 이 데이터는 PPTX의 네이티브 테이블(Table) 및 텍스트 상자로 생성하여, 리포트 출력 후에도 사용자가 파워포인트 내에서 직접 내용을 수정할 수 있도록 보장합니다.
#### 7.2 초정밀 클린 캡처 및 안정화 (Clean Capture & Stabilization)
내보내기 수행 시, 시스템은 실시간으로 **가상 스타일시트 주입(Virtual CSS Injection)****모사 데이터 동기화** 기술을 사용합니다.
- **UI 노이즈 제거**: 연결점(Port/Anchor), 선택 박스(Selection), 그리드 가이드 등을 캡처 순간에만 `display: none`으로 처리합니다.
- **적응형 안정화 지연 (1000ms Delay)**: 모델 업데이트(Base64 변환 등) 후 브라우저 DOM이 완전히 동기화되도록 1초의 대기 시간을 강제하여, 저사양 환경에서도 캡처 무결성을 보장합니다.
- **해상도 보정 (2x Scaling)**: 사용자 모니터의 DPI와 관계없이 항상 고품질 인쇄가 가능한 수준의 픽셀 밀도를 확보하기 위해 2배수 슈퍼 샘플링(Super Sampling) 캡처를 수행합니다.
#### 7.3 전수 속성 스캔 및 Base64 임베딩 (Recursive Asset Embedding)
SVG 내보내기의 핵심은 외부 자산의 완전한 자립화입니다.
- **재귀적 선택자 탐색**: 특정 태그에 국한되지 않고 노드의 모든 속성 트리를 스캔하여 외부 이미지 경로를 식별합니다.
- **속성 패리티(Parity)**: `xlink:href`와 일반 `href` 속성 모두에 동일한 Base64 데이터를 주입하여 크로스 브라우저/뷰어 호환성을 극대화합니다.
- **절대 좌표 매핑 (Zero-Clip)**: 캔버스의 현재 뷰포트 상태와 무관하게 `getCellsBBox`를 통한 실제 객체 영역을 자동 계산하고, 명시적인 `viewBox` 주입을 통해 상하좌우 짤림 현상을 원천 차단합니다.
상태, 환경 사양 등 무한한 메타데이터를 자유롭게 추가하고 수정할 수 있습니다.
- **데이터 활용성**: 모든 부가 정보는 JSON 데이터 모델에 정적/동적으로 포함되어 저장되므로, 설계가 완료된 후 즉시 인벤토리 보고서(Inventory Report) 생성, 검색 엔진 필터링, 혹은 외부 시스템과의 데이터 연동(API)에 활용됩니다.
- **설계 의도의 보존**: 이는 "그림"이 아닌 "설계 데이터" 그 자체를 보존함으로써, 설계자가 의도한 모든 엔지니어링 맥락이 유실 없이 영구적으로 관리됨을 의미합니다.
### 5.4 PM 지향적 설계 및 자산 관리 (PM-Centric Design & Asset Management)
엔지니어의 기술적 관점뿐 아니라, 프로젝트 매니저(PM)의 **의사결정 및 자산 관리 효율성**을 극대화합니다.
- **선제적 비즈니스 데이터 매핑**: 설계 초기 단계부터 각 객체에 벤더 정보, 단가(Price), 구매 일정, 담당 부서 등 관리 지표를 미리 입력할 수 있습니다.
- **원클릭 전수 관리**: "Inventory View" 및 "JSON/PPTX Export" 기능을 통해 프로젝트에 투입된 전체 자산 현황과 예상 비용을 한눈에 파악할 수 있으며, 이는 즉시 구매 발주서나 예산 보고서의 기초 자료로 변환됩니다.
- **전략적 가시성**: 시각적 토폴로지와 비즈니스 데이터가 결합됨으로써, PM은 단순히 "어떻게 연결되는가"를 넘어 "무엇이 얼마의 비용으로 구축되는가"에 대한 전략적 통찰을 실시간으로 확보할 수 있습니다.
### 5.5 REST API 기반의 생태계 통합 (REST API-Based Ecosystem Integration)
drawNET의 데이터 구조는 고립된 파일에 머물지 않고 외부 시스템과 유기적으로 소통할 수 있는 **프로그래밍 가능한 인터페이스(Programmable Interface)**를 지향합니다.
- **Native REST API 지원**: Flask 기반의 강력한 백엔드를 통해 토폴로지 데이터의 CRUD(Create, Read, Update, Delete)를 REST API로 실시간 제공합니다.
- **엔터프라이즈 시스템 연동**: 기업 내 CMDB(자산 관리 시스템), NMS(네트워크 모니터링), 혹은 클라우드 프로비저닝 엔진과의 양방향 데이터 동기화가 가능합니다.
- **자동화된 생명주기 관리**: 외부 스크립트를 통해 새로운 프로젝트를 자동 생성하거나, 실시간 모니터링 데이터를 API로 수신하여 캔버스상의 노드 상태(장애 발생 등)를 시각적으로 즉시 갱신하는 등 고도화된 자동화 워크플로우를 구현할 수 있는 강력한 무기를 갖추고 있습니다.
---
## 6. 엔지니어링 생산성: 지능형 단축키 시스템 (Engineering Productivity: Intelligent Hotkey System)
drawNET은 일반적인 드로잉 도구(Draw.io, Visio 등)와 차별화되는 **"전문 엔지니어 전용 워크스테이션"**으로서의 정체성을 단축키 시스템을 통해 증명합니다.
### 6.1 "Always-Enabled" 로직의 기술적 의도
일반적인 웹 도구는 텍스트 입력 중 시스템 단축킹(Undo/Redo 등)이 차단되거나 포커스 충돌을 일으킵니다. drawNET의 단축키 엔진은 `isTyping` 상태를 정밀하게 인지하되, 핵심적인 그래프 제어 액션에 대해서는 **전역적 우선순위(Override Priority)**를 부여합니다. 이는 수천 개의 속성을 동시에 수정하는 현업 환경에서 작업의 연속성을 완벽하게 보장하는 설계입니다.
### 6.2 Drawing Tool vs. Engineering Tool
- **드로잉 도구 (Draw.io, Visio)**: 시각적 표현이 목적이므로 마우스 기반의 정교한 배치를 우선시하며, 단축키는 보조 수단에 불과합니다.
- **엔지니어링 도구 (drawNET)**: 토폴로지 구성 속도가 핵심입니다. 따라서 **"Zero-Mouse"** 워크플로우를 지향하며, 로직 기반의 정렬(Alignment), 속성 복제(Format Painter), 컨텍스트 기반의 부모-자식 포함 로직을 단축키 하나로 수행할 수 있도록 최적화되어 있습니다.
### 6.3 현업 최적화 컨텍스트 핸들러
단순한 좌표 이동을 넘어, `Grid Snapping` 연동 이동, `Large/Small Step` 이동, 그리고 레이어 가시성 퀵 토글 등 현업 엔지니어가 토폴로지 구성 시 가장 빈번하게 수행하는 동작들을 모듈화된 핸들러(`handlers/`)로 구현하여 응답성을 극대화했습니다.
---
## 7. 전문 내보내기 서브시스템 및 하이브리드 리포팅 (High-Fidelity Export & Hybrid Reporting)
drawNET은 단순한 캔버스 캡처를 넘어, 엔지니어링 결과물의 시각적 완밀성과 데이터의 편집 가능성을 동시에 확보하는 **하이브리드 내보내기 전략**을 취합니다.
### 7.1 하이브리드 이미지-객체 모델 (Hybrid Image-Object Model)
PPTX 리포트 생성 시, 시스템은 두 가지 서로 다른 렌더링 경로를 결합합니다.
1. **Topology View (High-Res Snapshot)**: 복잡한 선(Path) 라우팅, SVG 그림자, 반투명 그룹 등은 PPTX 네이티브 객체로 변환 시 시각적 손실이 큽니다. 이를 방지하기 위해 200% 스케일의 고해상도 PNG 스냅샷으로 캔버스를 캡처하여 삽입합니다.
2. **Inventory Data (Native PPTX Objects)**: 장비 목록, 프로젝트 정보, 요약 통계는 수정이 용이해야 합니다. 따라서 이 데이터는 PPTX의 네이티브 테이블(Table) 및 텍스트 상자로 생성하여, 리포트 출력 후에도 사용자가 파워포인트 내에서 직접 내용을 수정할 수 있도록 보장합니다.
### 7.2 초정밀 클린 캡처 메커니즘 (Clean Capture Mechanism)
내보내기 수행 시, 시스템은 실시간으로 **가상 스타일시트 주입(Virtual CSS Injection)** 기술을 사용하여 시각적 노이즈를 제거합니다.
- **UI 노이즈 제거**: 연결점(Port/Anchor), 선택 박스(Selection), 그리드 가이드 등을 캡처 순간에만 `display: none`으로 처리하여 설계 데이터 본연의 모습만 선명하게 담아냅니다.
- **해상도 보정 (2x Scaling)**: 사용자 모니터의 DPI와 관계없이 항상 고품질 인쇄가 가능한 수준의 픽셀 밀도를 확보하기 위해 2배수 슈퍼 샘플링(Super Sampling) 캡처를 수행합니다.
### 7.3 레이어 기반 수직적 리포팅 (Layer-Aware Vertical Reporting)
전체 토폴로지뿐 아니라, 프로젝트에 정의된 각 레이어(물리, 논리, 보안 등)를 개별적으로 분석하여 슬라이드를 생성합니다.
- **동적 가시성 제어**: 각 레이어별 전용 슬라이드를 캡처할 때, 시스템은 백그라운드에서 레이어 가시성을 순차적으로 토글(Toggle)하고 필터를 적용하여 해당 도메인의 객체들만 깨끗하게 분리된 자동 리포트를 구성합니다.
#### 7.4 엔지니어링 실무 데이터 매핑 (Engineering Data Mapping)
- **식별자 기반 매핑 (ID-Key Linkage)**: drawNET의 모든 객체는 고유 UUID를 Primary Key로 가집니다. 이는 실무 엔지니어가 보유한 방대한 외부 데이터(라우팅 테이블, 방화벽 룰 등)와 시각적 도면을 VLOOKUP이나 데이터 조인으로 즉시 연결하는 매개체가 됩니다.
- **경량 메타데이터 레이어 (Description & Tags)**: 도면의 가벼움을 유지하면서도 실무에 꼭 필요한 컨텍스트를 `Tags``Description`으로 캡처합니다. 이는 "무거운 데이터 관리는 외부 전문 툴에 맡기고, 도면은 맥락과 연결 고리만 제공한다"는 실무적 철학을 반영합니다.
- **BOM 및 인벤토리 자동 생성**: 인벤토리 내 '소속 그룹' 정보를 포함하여, 내보낸 즉시 자산 관리 대장(Excel)으로 활용 가능한 고순도 데이터를 제공합니다.
---
## 8. Connectivity & Simulation Architecture (논리적 연결 계층화)
drawNET은 물리적 자산 배치와 논리적 연결 시뮬레이션을 분리하기 위해 **'레이어 타입 기반 제약(Layer-Type Constraints)'** 아키텍처를 채택합니다.
### 8.1 레이어 타입 정의
- **Standard (Physical)**: 장비(Node)와 연결(Edge)이 공존하는 기본 레이어.
- **Logical (Simulation)**: 연결선(Edge)만 허용되는 특수 레이어. 신규 노드 생성이 차단되며, 복제 시 노드는 필터링되고 연결 관계만 전이됩니다.
### 8.2 데이터 일관성 및 시뮬레이션 원칙
- **SSoT (Single Source of Truth)**: 장비의 마스터 정보는 물리 레이어에 위치하며, 논리 레이어의 선들은 서로 다른 레이어에 속한 노드들을 참조(UUID 기반)하여 가상의 망 구성을 형성합니다.
- **포맷 페인터(Style Propagation)**: 클립보드 데이터를 최우선으로 하는 `Default-Strict` 로직을 통해 개체의 스타일 정보가 유실 없이 전파되도록 보장합니다.
- **자기 치유형 상태 관리**: 브라우저 및 OS 레벨의 이벤트 유실에 대비하여, 인터랙션 발생 시마다 실제 모디파이어(Ctrl/Cmd) 상태를 재검증하는 안정성 프로토콜을 운영합니다.
## 9. AI Ready: 구조화된 그래프 데이터의 가치 (AI Ready: The Value of Structured Data)
drawNET은 단순히 도면을 "그리는" 도구가 아니라, **AI가 즉시 이해하고 분석할 수 있는 고정밀 데이터셋**을 생성하는 플랫폼입니다.
### 9.1 데이터 고정밀도 (Data High-Fidelity)
기존의 PDF나 이미지(OCR 기반) 도면 분석은 시각적 모호성으로 인해 연결 관계의 왜곡이나 오인식 확률이 높습니다. drawNET의 JSON-Native 구조는 모든 연결을 UUID와 참조 무결성(Referential Integrity)으로 정의하므로, AI에게 **"정답(Ground Truth)"**을 제공합니다. 이는 AI 모델의 학습 및 분석 정확도를 비약적으로 높여줍니다.
### 9.2 지능형 설계 감사 및 예측 (Intelligent Auditing & Prediction)
구조화된 데이터 모델은 차세대 AI 엔진과 결합하여 다음과 같은 가치를 창출합니다:
- **자동 설계 검증 (Automated Auditing)**: 설계된 토폴로지가 기업의 보안 정책이나 표준망 구성 원칙을 준수하는지 AI가 실시간으로 감시하고 이상 징후를 탐지합니다.
- **장애 시뮬레이션 (Impact Analysis)**: 특정 노드 장애 시 전체 서비스에 미치는 영향도를 그래프 알고리즘으로 자동 계산하여 최적의 백업 경로를 제안합니다.
- **비용 최적화 (TCO Optimization)**: 입력된 단가와 장비 스펙 데이터를 AI가 분석하여, 운용 효율은 유지하면서도 구축 비용을 최소화할 수 있는 설계 최적화 모델을 제시합니다.
### 9.3 그래프 알고리즘과의 완벽한 호환성
본 프로젝트가 생성하는 데이터는 **그래프 이론(Graph Theory)**을 따르는 인접 리스트(Adjacency List) 구조로 즉시 변환 가능합니다. 이는 LLM(대형 언어 모델)뿐만 아니라 GNN(Graph Neural Networks)과의 결합을 통해 네트워크 인프라의 미래를 설계하는 데 가장 적합한 아키텍처임을 증명합니다.
### 9.4 네트워크 가상 시뮬레이션 및 디지털 트윈 (Network Virtual Simulation & Digital Twin)
drawNET의 데이터는 단순한 시각화를 넘어, 가상 환경에서의 **패킷 흐름 시뮬레이션**을 가능케 하는 디지털 트윈의 기반이 됩니다.
- **라우팅 루프 및 블랙홀 탐지 (Loop & Blackhole Detection)**: 엔터프라이즈 네트워크의 고질적인 문제인 라우팅 루프를 설계 단계에서 사전에 차단합니다. 객체의 `tags`에 포함된 라우팅 정보와 `ip` 속성을 그래프 경로 탐색 알고리즘과 결합하여, 논리적 순환 경로(Loop)나 목적지 없이 사라지는 경로(Blackhole)를 AI가 0.1초 만에 전수 검사합니다.
- **물리-논리 정합성 검증**: 물리적 케이블 연결(Edge)과 논리적 라우팅 정책(Metadata) 사이의 불일치를 잡아내어, "그림과 실제 설정이 다른" 휴먼 에러를 원천적으로 방지합니다.
### 9.5 AI 범용성 및 미래 지향성 (Universal AI-Agnostic Architecture)
drawNET이 생성하는 데이터 모델은 특정 AI 기술에 종속되지 않는 **"범용 AI 데이터 언어"**로서의 가치를 지닙니다.
- **Cross-AI Compatibility**: OpenAI(GPT), Anthropic(Claude), Meta(Llama) 등 모든 주요 AI 모델들은 시각적 이미지보다 정형화된 JSON 데이터에서 강력한 추론 능력을 발휘합니다. drawNET은 어떤 AI가 등장하더라도 즉시 협업할 수 있는 완벽한 인터페이스를 미리 갖추고 있습니다.
- **데이터 주권과 보안 (Data Sovereignty)**: 클라우드 기반 AI를 사용할 수 없는 고도의 보안 환경에서도, 표준 JSON 형식의 프로젝트 파일을 로컬 AI 엔진(On-premise LLM)이 직접 분석하게 함으로써 데이터 외부 유출 없이도 지능형 설계를 수행할 수 있습니다.
- **공공 및 국가 중요 시설 최적화**: 망 분리 환경이 필수적인 공공기관, 금융, 국방 분야에서 폐쇄형 LLM과의 결합을 통해 **보안 가이드라인 준수 여부(Compliance Audit)**를 자동 검증할 수 있습니다. 이는 인적 과실로 인한 국가적 보안 사고를 설계 단계에서 원천 차단하는 가장 강력한 보안 설계 아키텍처임을 보장합니다.
- **미래를 위한 확장팩**: 현재 입력된 `tags`, `ip`, `metadata` 등은 미래의 강력한 AI 에이전트가 네트워크 인프라를 자동으로 정밀하게 제어(Autonomous Networking)할 수 있게 만드는 핵심 데이터 전구체(Precursor) 역할을 수행합니다.
---
## 10. 미래 전망 및 지향점 (Future Direction)
1. **AI Topology Auditor**: 저장된 JSON 데이터를 분석하여 네트워크 설계상의 병목 구간이나 단일 장애점(SPOF)을 AI가 자동으로 식별하는 기능을 계획하고 있습니다.
2. **Visual Diff Engine (Next Gen)**: 두 개의 프로젝트 JSON 파일을 비교하여 노드 위치 변화, 속성 변화를 캔버스상에 시각적으로 강조(Highlight)하는 도구로 발전시킬 것입니다.
3. **Master Model Definition**: 하나의 마스터 모델에서 다양한 관점의 서브 그래프(View)를 동시에 생성하고 동기화하는 엔터프라이즈 협업 기능을 지향합니다.
---
*최초 공식 릴리즈: 2026-03-20 (v1.0 Premium Edition - New Launch)*
*작성자: Antigravity AI Implementation Team (drawNET Core)*
+23
View File
@@ -0,0 +1,23 @@
# drawNET Bug Fix Log 🐞 index
이 문서는 drawNET 개발 과정에서 발생한 버그 수정 이력을 날짜별로 요약 및 아카이브합니다.
---
## 📅 아카이브 목록 (By Date)
- **[2026-03-21](./bug_reports/2026-03-21.md)**: 리치 카드 이슈, 데이터 정합성(Z-Index, 유실 복구), 사이드바 수동 저장 모델 도입 등 (총 19건)
- *추가 예정...*
---
## 🔍 주요 카테고리별 요약
| 카테고리 | 주요 사례 | 누적 건수 |
| :--- | :--- | :--- |
| **UX/UI** | 사이드바 포커스 유지, Alt+Enter, 실시간 동기화 | 5건 |
| **Syntax/Load** | JSON 문법 오류, 중복 식별자, 구문 에러 | 4건 |
| **Stability/Data** | 데이터 복구(Pos/Label), Z-Index 계층, 엣지 보존 | 10건 |
---
마지막 업데이트: 2026-03-21
+133
View File
@@ -0,0 +1,133 @@
# drawNET Bug Report - 2026-03-21 🐞
이 문서는 2026년 3월 21일 발생한 버그 및 장애와 그에 따른 조치 사항을 기록합니다.
---
### [Instance #2255] - Sidebar Focus Loss & Explicit Save Refactoring
- **현상**: 사이드바에서 라벨 수정 시 포커스 유실로 인한 타이핑 불가능 현상 및 자동 저장에 의한 의도치 않은 데이터 경합 가능성 제기.
- **원인**: 실시간 `input` 및 자동 `change`(blur) 이벤트가 시각 속성 변경 리스너와 충돌하여 DOM 리렌더링 발생.
- **조치**:
1. **명시적 저장 모델(Explicit Apply)** 도입: 텍스트 관련 필드는 오직 **Enter** 또는 **[적용]** 버튼 클릭 시에만 저장되도록 변경하여 입력 중 데이터 무결성 보장.
2. 사이드바 내부 요소에 포커스가 있을 경우 그래프 외부 변화에 의한 리렌더링을 차단하여 포커스 연속성 확보.
- **현상**: 라벨 위치를 "하(bottom)"로 설정해도 저장 후 불러오면 다시 "상(top)"으로 초기화됨.
- **원인**: `layers.js` 내부에 라벨 위치 기본값이 `bottom`으로 하드코딩되어 강제 적용되었으며, 일부 객체 생성 시 `label_pos` 데이터 주입 누락.
- **조치**: `layers.js` 하드코딩 제거, `system.js`에서 생성 시 기본값(장비-하, 그룹-상) 강제 주입, 유틸리티 함수 보강을 통한 정합성 확보.
### [Instance #2245] - Sidebar Display Stale During Group Rename (Recursive & Logic Fix)
- **현상**: 그룹 이름을 변경해도 해당 그룹에 속한 하위 개체의 사이드바 내 [상위] 정보가 즉시 바뀌지 않음.
- **원인**:
1. **저장 조건부 누락**: 부모 노드가 바뀌지 않은 단순 속성(이름 등) 변경 시 `saveUpdates` 호출이 누락되어 저장이 실행되지 않음.
2. **포커스 가드 충돌**: 사이드바 내부에서 편집 중일 때 리렌더링을 차단하는 로직이 적용(Apply) 시점의 갱신마저 막아버림.
3. **비동기 타이핑 불일치**: 엔진의 데이터 확정 전 UI 렌더링 시도로 인한 구형 데이터 출력.
- **조치**:
1. `handleNodeUpdate` 내 저장 로직을 평탄화하여 **속성 변경 시 항상 저장**되도록 수정.
2. **재귀적 전파(Recursive Propagation)** 도입: 그룹명 변경 시 하위 모든 개체(`getDescendants`)에 동기화 타임스탬프를 주입하여 하위 개체들의 변경 이벤트를 강제 트리거함.
3. `safeRender` 포커스 가드 최적화: `INPUT/TEXTAREA`가 활성화된 경우만 스킵하도록 정규화하고 `setTimeout(0)`으로 렌더링 시기를 튜닝함.
- **현상**: 불러온 파일의 일부 그룹 라벨이 화면 출력값과 사이드바 표시값이 서로 다름 (예: Storage vs 그룹).
- **원인**: 과거 버전의 저장 로직 오류로 인해 `attrs``data` 필드 간의 불일치가 파일 내에 고착화됨.
- **조치**: `json_handler.js` 마이그레이션 필터를 추가하여, 로드 시 `data.label`을 원본 소스로 삼아 시각적 라벨을 강제 동기화함.
### [Instance #2235] - Destructive Object Disappearance (Style Corrupt)
- **현상**: 레이어 설정 변경 시 객체가 갑자기 투명해지거나 색상이 사라지는 현상.
- **원인**: `layers.js`에서 투명도 적용 시 `setAttrs`를 사용하여 기존의 모든 커스텀 스타일(fill, stroke 등)을 덮어써버림.
- **조치**: `attr()` 개별 호출 방식으로 변경하여 기존 스타일 유산을 완벽히 보존함.
### [Instance #2230] - Missing Coordinates Recovery (Pos/Size)
- **현상**: 일부 객체가 좌표값 누락으로 인해 (0,0)에 배치되거나 아예 보이지 않음.
- **원인**: 복사/붙여넣기 및 엔진 전환 과정에서 X6 표준 좌표 필드가 유실된 데이터 잔재.
- **조치**: `json_handler.js``data.pos` 기반 좌표 자동 복구 로직을 주입하여 유실된 개체들을 정상 위치로 소환함.
### [Instance #2225] - Edge Clipping & Selection Difficulty (Z-Index)
- **현상**: 그룹 위에 선을 그으면 그룹 뒤로 숨어서 클릭이 안 됨. 단축키 연결 시에도 동일 현상 반복.
- **원인**: 엣지 기본 Z-Index가 5로 설정되어, 레이어 오프셋이 적용된 그룹보다 하단에 배치됨.
- **조치**: 전체 계층 구조 재설계 `Group(1~19) < Edge(30) < Node(50)` 및 마우스/단축키 생성 경로 동기화.
### [Instance #2220] - Accidental Reset of Edge Vertices
- **현상**: 새로운 장비를 연결하면 기존에 예쁘게 꺾어 놓았던 선들이 모두 일직선으로 초기화됨.
- **원인**: `applyLayerFilters` 함수 내에 모든 엣지의 버텍스를 강제로 비우는 과도한 '자동 복구' 로직이 포함되어 있었음.
- **조치**: 해당 파괴적인 초기화 로직(`setVertices([])`)을 삭제하여 사용자 커스텀 경로 보존. (Instance #1480 정정 사항)
---
### [Instance #2202] - Label Reverts when Changing Label Position (Select Sync)
- **현상**: 라벨 이름을 수정한 직후(아직 저장되기 전) 라벨 위치(`select`)를 변경하면 이름이 이전 상태로 되돌아감.
- **원인**: 모든 입력 요소에 동일한 `debounce`(300ms)가 적용되어 있어, 상호작용 간 데이터 경합 발생 및 `renderProperties`에 의한 DOM 덮어쓰기 현상.
- **조치**: `select`, `checkbox`, `color` 등 상태 선택형 요소는 `change` 이벤트 발생 시 즉시 저장(`applyChanges`)되도록 변경하여 데이터 동기화 보장.
### [Instance #2108] - Inconsistent Line Thickness (Style vs Selection)
- **현상**: 구성도 작성 시 선 두께가 일정하지 않게 보임.
- **원인**:
1. 선택 시 강조 효과(3px)와 일반 상태(2px)의 차이.
2. '굵은 선' 스타일이 UI에는 있으나 실제 두께(4px) 반영 로직이 누락됨.
- **조치**:
1. '굵은 선(double)' 선택 시 실제 4px가 적용되도록 매핑 로직 업데이트.
2. 속성창에 '선 두께(Width)' 수치 입력 필드를 추가하여 개별 커스터마이징 지원.
### [Instance #2010] - ID & Label Confusion (Initial Data Setup)
- **현상**: 그룹이나 노드 생성 시 이름이 `group_xxxx`와 같은 ID 형식으로 표시되며, 수정 후 특정 상황에서 다시 ID 값으로 되돌아가는 것처럼 보임.
- **원인**: `drop.js`에서 노드 생성 시 ID와 Label을 동일한 랜덤 문자열로 초기화했기 때문에 발생한 혼동.
- **조치**: 초기 라벨을 '그룹', '사각형' 등 친숙한 한글 명칭(다국어 대응)으로 설정하도록 변경하고, 사이드바에서 ID를 읽기 전용 필드로 명시적으로 분리 노출함.
### [Instance #1968] - Data Reverted During Move after Rename (Blur vs Drag)
- **현상**: 그룹 이름을 수정한 직후 다른 곳을 클릭하지 않고 바로 드래그하여 이동하면 이름이 이전 값으로 되돌아감.
- **원인**: 사이드바가 `change`(포커스 해제) 시점에 저장하는데, 드래그 시 발생하는 리렌더링 이벤트가 `change`보다 먼저 발생하여 이전 데이터를 불러옴.
- **조치**: 사이드바 텍스트 입력 이벤트를 `input`으로 변경하고 300ms 디바운싱을 적용하여 실시간 동기화 구현.
### [Instance #1940] - JSON Syntax Error in Locale File (Stray Backticks)
- **현상**: 앱 구동 시 `[drawNET:CRITICAL] Failed to load locales` 에러 발생하며 모든 텍스트가 키값으로 표시됨.
- **원인**: `ko.json` 수정 과정에서 AI 도구가 파일 끝에 마크다운 기호(```)를 잘못 삽입하여 유효하지 않은 JSON 형식이 됨.
- **조치**: `ko.json` 파일 끝의 불필요한 문자를 제거하고 구조 정상화.
### [Instance #1931] - Property Handler Syntax Error (Accidental Deletion)
- **현상**: 노드 속성 수정 시 변경사항이 반영되지 않거나 콘솔에 `SyntaxError` 발생.
- **원인**: '에셋 경로' 필드를 제거하는 과정에서 `handlers/node.js` 속성 추출 객체의 괄호 및 인접 필드(color, fill 등)를 실수로 대량 삭제함.
- **조치**: 삭제된 모든 필드 로직을 원래대로 복구하고 의도했던 `assetPath` 추출부만 정교하게 제거.
### [Instance #1860] - Rich Card Sidebar UI Inconsistency
- **현상**: 리치 텍스트 카드의 속성창 디자인이 기존 '컴팩트 UI' 표준(토글 스위치, 정렬된 입력창 등)과 다르게 투박하게 표시되고 정보 밀도가 낮음.
- **원인**: 신규 객체 전용 템플릿 작성 시 표준 CSS 클래스(`prop-input`, `toggle-switch`) 적용 누락 및 일부 스타일(`input-with-color`) 미정의.
- **조치**: `rich_card.js` 템플릿을 표준 컴포넌트 구조로 전면 재작성하고, `properties_sidebar.css`에 부족한 레이아웃 스타일 보강.
### [Instance #1840] - Rich Card Syntax Error (Duplicate Identifier)
- **현상**: 리치 카드 선택 시 콘솔에 `Uncaught SyntaxError: Identifier 'renderRichContent' has already been declared` 발생 및 사이드바 작동 중단.
- **원인**: `styles/utils.js`에서 함수를 임포트했음에도 불구하고, 로컬 핸들러 파일에도 동일한 가명 함수가 남아있어 충돌 발생.
- **조치**: `handlers/rich_card.js` 하단의 중복된 `renderRichContent` 함수를 제거하고 공용 유틸리티 함수만 사용하도록 수정.
### [Instance #1692] - 다중 선택 해제 시 시각적 잔상(Focus) 유지
- **현상**: `Ctrl+Click`으로 다중 선택 객체 중 일부를 해제해도 캔버스상에 점선 테두리(Focus)가 남아 있음.
- **원인**: X6의 `Selection` 또는 `Transform` 플러그인이 추가한 `boundary` 도구가 `unselected` 이벤트 시 자동으로 제거되지 않거나 커스텀 도구 관리에 밀려 잔류함.
- **조치**: `selection.js``unselected` 이벤트 핸들러에서 노드/엣지 해제 시 `removeTools()`를 명시적으로 호출하도록 강화.
### [Instance #1684] - `removeLabel` 런타임 에러
- **현상**: 엣지 선택 해제 시 `cell.removeLabel is not a function` 에러 발생.
- **원인**: 일부 X6 환경에서 엣지 객체의 단축 함수가 유실되거나 차단되는 문제.
- **조치**: `setLabels``filter`를 사용하여 명시적으로 해당 ID의 라벨을 제외한 리스트를 재설정함.
### [Instance #1640] - 하이라이트 플러그인 의존성 에러
- **현상**: 시작/대상 앵커 호버 시 `createExecutionContext``unhighlight` 함수를 찾을 수 없다는 TypeError 발생.
- **원인**: AntV X6의 Highlight 플러그인 의존성 문제.
- **조치**: 외부 플러그인 의존성을 제거하고, 노드의 본체(`body`) 속성(`attr`)을 직접 제어하여 강조 효과를 주는 방식으로 로직 선회.
### [Instance #1628] - `addLabel` 런타임 에러
- **현상**: 엣지에 라벨 추가 시 `cell.addLabel is not a function` 에러 발생.
- **원인**: X6 특정 버전에서 엣지 객체의 믹스인(Mixin) 상태에 따라 단축 함수가 인식되지 않는 문제.
- **조치**: `getLabels()` / `setLabels()` 패턴으로 변경하여 호환성 확보.
### [Instance #1505] - 직선 라우팅 강제 꺾임 현상
- **현상**: 라우터를 제거했음에도 선이 90도로 꺾여 연결됨.
- **원인**: X6의 방향성 앵커(`top`, `left` 등)가 경로에 영향을 미침.
- **조치**: '직선' 모드일 경우 앵커를 방향성이 없는 `center`로 강제 전환.
### [Instance #1480] - 레이어 필터링 시 라우팅 설정 유실 (정정됨)
- **현상**: 레이어나 줌 변경 시 다시 꺾인 상태로 회귀함.
- **원인**: 레이어 'Auto-Repair' 로직이 Vertices를 초기화하지 않음.
- **조치**: (구) `setVertices([])` 추가 -> (현) 해당 로직이 사용자 데이터 파괴를 유발하여 최종 삭제 완료 (Instance #2220 참조).
### [Instance #1466] - Logger 참조 에러
- **현상**: `logger is not defined` 발생.
- **원인**: `edge.js` 내 모듈 임포트 누락.
- **조치**: `logger` 모듈 임포트 추가.
---
*아카이브 관리자: drawNET AI Assistant*
@@ -0,0 +1,17 @@
# drawNET Bug Report - 2026-03-22 🐞
이 문서는 2026년 03월 22일 발생한 설계 취약점 보완 및 그에 따른 조치 사항을 기록합니다.
---
### [Instance #0001] - 연결선(Edge)의 레이어 정보 유추 의존성 해결
- **현상**: 연결선 데이터 자체에 레이어 정보가 없어, 특정 연결선이 크로스 레이어인지 판단하려면 반드시 소스/타겟 노드를 그래프에서 검색해야 함. 이는 외부 도구(LLM 등) 연동 시 데이터 독립성을 저하시키고, 대규모 도면에서 필터링 성능 저하를 유발함.
- **원인**: 초기 설계 시 "노드 ID가 있으니 나중에 조회하면 된다"는 유추 방식에 의존함. 데이터 아키텍처 관점에서 객체 단독으로는 자신의 상태(Cross-layer 여부 등)를 완벽히 설명하지 못하는 불완전한 상태였음.
- **조치**:
1. `static/js/modules/graph/interactions/edges.js`: `edge:connected` 이벤트 발생 시 소스/타겟 노드의 `layerId`를 직접 조회하여 연결선 데이터(`source_layer`, `target_layer`, `is_cross_layer`)에 명시적으로 기록하도록 수정.
2. `static/js/modules/graph/io/json_handler.js`: 기존 도면(Legacy data) 로드 시 누락된 레이어 메타데이터를 자동으로 계산하여 보정하는 자가 치유(Self-healing) 로직 추가.
3. `6개월 뒤의 나`를 위해 각 관련 파일에 작업 배경 및 이유를 주석으로 명시.
- **관련 이전 이슈**: 없음.
모든 항목은 압축하지 말고, 이전 리포트들보다 더 자세하거나 최소한 동일한 상세도로 작성할 것.
절대 요약·생략·간략화하지 마. 각 항목에 구체적인 코드 조각, 이벤트 이름, 함수명, 속성명 등을 최대한 포함.
@@ -0,0 +1,17 @@
# drawNET Bug Report - 2026-03-22 🐞
이 문서는 2026년 03월 22일 발생한 UI 불일치 문제 해결 및 그에 따른 조치 사항을 기록합니다.
---
### [Instance #0002] - 파비콘(Favicon)과 서비스 로고 아이콘 불일치 해결
- **현상**: 브라우저 탭에 표시되는 파비콘과 서비스 내부 사이드바 상단의 "dN" 로고 아이콘이 서로 달라 사용자 경험(UX)에 일관성이 없으며, 기존 파비콘의 심미성이 떨어짐.
- **원인**: 초기 개발 시 기본 제공되는 `favicon.ico`를 그대로 사용하였으나, 서비스 브랜드 아이덴티티가 "dN" 로고(Blue/Indigo Gradient)로 확립된 이후에도 파비콘이 갱신되지 않음.
- **조치**:
1. **아이콘 생성**: 사이드바 `.logo-icon`의 CSS 스타일(135도 그라데이션, #3b82f6 ~ #6366f1, 흰색 "dN" 텍스트)을 그대로 반영한 고해상도 PNG 이미지를 생성.
2. **파일 교체**: `static/favicon.png` 경로에 새로운 파비콘 이미지 저장.
3. **코드 반영**: `templates/index.html``<link rel="icon">` 태그를 새로운 PNG 파비콘으로 교체하여 즉시 반영.
- **관련 이전 이슈**: 없음.
모든 항목은 압축하지 말고, 이전 리포트들보다 더 자세하거나 최소한 동일한 상세도로 작성할 것.
절대 요약·생략·간략화하지 마. 각 항목에 구체적인 코드 조각, 이벤트 이름, 함수명, 속성명 등을 최대한 포함.
@@ -0,0 +1,16 @@
# drawNET Bug Report - 2026-03-22 🐞
이 문서는 2026년 03월 22일 발생한 다크모드 가시성 문제 해결 및 그에 따른 조치 사항을 기록합니다.
---
### [Instance #0003] - 다크모드에서 도움말(온보딩 가이드) 가시성 불량 해결
- **현상**: 다크모드 활성화 시, 도움말 모달 내의 텍스트가 배경색과 비슷한 어두운 색으로 표시되어 읽기가 매우 힘듦. (사용자 제보 이미지 확인됨)
- **원인**: `static/js/modules/ui/help_modal.js` 내부의 HTML 템플릿에서 텍스트 색상(`#1e293b`, `#334155` 등)과 배경색을 하드코딩된 인라인 스타일로 정의함. 이로 인해 테마가 바뀌어도 색상이 고정되어 대비(Contrast)가 확보되지 않음.
- **조치**:
1. **테마 변수 적용**: 하드코딩된 색상을 `base.css`에 정의된 CSS 변수(`var(--text-color)`, `var(--sub-text)`, `var(--panel-border)`, `var(--item-bg)`, `var(--accent-color)`)로 전면 교체.
2. **가독성 보정**: 정보 박스(`manual/guide.md` 안내)의 배경을 `var(--item-bg)`로 변경하여 다크모드에서도 자연스럽고 선명하게 보이도록 수정.
- **관련 이전 이슈**: 없음.
모든 항목은 압축하지 말고, 이전 리포트들보다 더 자세하거나 최소한 동일한 상세도로 작성할 것.
절대 요약·생략·간략화하지 마. 각 항목에 구체적인 코드 조각, 이벤트 이름, 함수명, 속성명 등을 최대한 포함.
@@ -0,0 +1,19 @@
# drawNET Bug Report - 2026-03-22 🐞
이 문서는 2026년 3월 22일 발생한 `help_modal.js` 내의 ReferenceError 해결 및 그에 따른 조치 사항을 기록합니다.
---
### [Instance #2300] - ReferenceError: t is not defined in help_modal.js
- **현상**: 도움말 버튼(`help-btn`) 클릭 시 도움말 모달(Help Modal)이 열리지 않고, 브라우저 콘솔에 `Uncaught (in promise) ReferenceError: t is not defined` 에러가 발생하며 인터페이스가 멈추는 현상.
- **원인**:
1. 마크다운 렌더러 라이브러리(`marked.js`) 통합을 위한 `help_modal.js` 리팩토링 과정에서 실수로 상단의 `import { t } from '../i18n.js';` 구문을 삭제함.
2. 모달 템플릿 코드 내에서 다국어 번역 함수인 `${t('help_center')}` 등을 호출할 때 해당 함수가 정의되지 않아 런타임 에러가 발생함.
- **조치**:
1. `static/js/modules/ui/help_modal.js` 파일 상단에 누락된 `t` 함수 임포트 구문을 즉시 복구함.
2. 모달 내의 모든 `t()` 호출부가 정상적으로 바인딩되었는지 검증하고, 모달이 정상적으로 팝업되는 것을 확인함.
3. 향후 리팩토링 시 필수 의존성(Dependency) 누락 여부를 체크하도록 코드 리뷰 프로세스 보강.
- **관련 이전 이슈**: 없음.
---
*아카이브 관리자: drawNET AI Assistant*
@@ -0,0 +1,19 @@
# drawNET Bug Report - 2026-03-22 🐞
이 문서는 2026년 3월 22일 발생한 오브젝트 스튜디오 404 에러 해결 및 그에 따른 조치 사항을 기록합니다.
---
### [Instance #2310] - 404 Resource Not Found in Object Studio
- **현상**: 오브젝트 스튜디오(Object Studio) 접속 시 다국어 파일(`i18n.js`)과 파비콘(`favicon.ico`)을 찾을 수 없다는 404 에러와 함께 UI의 버튼 글자가 번역되지 않은 상태(Raw Key)로 노출됨.
- **원인**:
1. **경로 오표기**: `static/js/modules/studio/` 디렉토리 내의 `renderer.js``actions.js`에서 `../../i18n.js` 경로를 사용함. 실제 파일은 `../i18n.js`(`static/js/modules/i18n.js`)에 위치해 있어 상대 경로 계산 오류 발생.
2. **파비콘 링크 누락**: `studio.html` 파일에 명시적인 파비콘 링크가 없어 브라우저가 기본값인 `/favicon.ico`를 호출함.
- **조치**:
1. `renderer.js``actions.js`의 임포트 경로를 `../i18n.js`로 일괄 수정하여 다국어 모듈 로딩 정상화.
2. `studio.html` 상단에 통합 로고(`logo.svg`)를 파비콘으로 사용하는 링크 태그 추가.
3. 스튜디오 전체 리소스 로딩 상태를 재점검하여 누락된 정적 파일이 없음을 확인.
- **관련 이전 이슈**: Instance #2300 (유사한 Import 문제)
---
*아카이브 관리자: drawNET AI Assistant*
+80
View File
@@ -0,0 +1,80 @@
# drawNET Bug Report - 2026-03-22 🐞
이 문서는 2026년 3월 22일 발생한 버그 및 장애와 그에 따른 조치 사항을 기록합니다.
---
### [Instance #2310] - 404 Resource Not Found in Object Studio
- **현상**: 오브젝트 스튜디오(Object Studio) 접속 시 다국어 파일(`i18n.js`)과 파비콘(`favicon.ico`)을 찾을 수 없다는 404 에러와 함께 UI의 버튼 글자가 번역되지 않은 상태(Raw Key)로 노출됨.
- **원인**:
1. **경로 오표기**: `static/js/modules/studio/` 디렉토리 내의 `renderer.js``actions.js`에서 `../../i18n.js` 경로를 사용함. 실제 파일은 `../i18n.js`(`static/js/modules/i18n.js`)에 위치해 있어 상대 경로 계산 오류 발생.
2. **파비콘 링크 누락**: `studio.html` 파일에 명시적인 파비콘 링크가 없어 브라우저가 기본값인 `/favicon.ico`를 호출함.
- **조치**:
1. `renderer.js``actions.js`의 임포트 경로를 `../i18n.js`로 일괄 수정하여 다국어 모듈 로딩 정상화.
2. `studio.html` 상단에 통합 로고(`logo.svg`)를 파비콘으로 사용하는 링크 태그 추가.
3. 스튜디오 전체 리소스 로딩 상태를 재점검하여 누락된 정적 파일이 없음을 확인.
### [Instance #2300] - ReferenceError: t is not defined in help_modal.js
- **현상**: 도움말 버튼(`help-btn`) 클릭 시 도움말 모달(Help Modal)이 열리지 않고, 브라우저 콘솔에 `Uncaught (in promise) ReferenceError: t is not defined` 에러가 발생하며 인터페이스가 멈추는 현상.
- **원인**:
1. 마크다운 렌더러 라이브러리(`marked.js`) 통합을 위한 `help_modal.js` 리팩토링 과정에서 실수로 상단의 `import { t } from '../i18n.js';` 구문을 삭제함.
2. 모달 템플릿 코드 내에서 다국어 번역 함수인 `${t('help_center')}` 등을 호출할 때 해당 함수가 정의되지 않아 런타임 에러가 발생함.
- **조치**:
1. `static/js/modules/ui/help_modal.js` 파일 상단에 누락된 `t` 함수 임포트 구문을 즉시 복구함.
2. 모달 내의 모든 `t()` 호출부가 정상적으로 바인딩되었는지 검증하고, 모달이 정상적으로 팝업되는 것을 확인함.
3. 향후 리팩토링 시 필수 의존성(Dependency) 누락 여부를 체크하도록 코드 리뷰 프로세스 보강.
### [Instance #2290] - Incorrect Filtering in PPTX Details Slide
- **현상**: PPTX 내보내기 시 "객체별 상세 내용 및 주석 목록" 슬라이드에서 실제 노드(오브젝트)는 보이지 않고, UUID 형태의 이름을 가진 선(edge)들만 대거 나열되는 현상.
- **원인**:
1. 상세 목록 필터가 `description` 속성이 있는 객체만 찾도록 되어 있어, 설명이 없는 일반 노드들은 모두 제외됨.
2. 수동 경로 오프셋(`routing_offset`)이 있는 엣지들이 필터에 포함되면서, 라벨이 없는 일반 엣지들이 UUID와 함께 노출됨.
- **조치**:
1. 상세 목록 필터를 개선하여 **모든 일반 노드**를 포함하도록 수정 (단, 그룹은 설명이 있을 때만 포함).
2. 엣지는 **사용자 정의 라벨이나 설명이 있을 때만** 포함하도록 변경하여 무의미한 항목 제거.
3. 라벨이 없는 엣지의 경우 UUID 대신 `[시작노드] -> [대상노드]` 형태의 읽기 쉬운 이름을 자동 생성하도록 보완.
### [Instance #2280] - Asset Filter Modal Invisibility
- **현상**: 왼쪽 에셋 라이브러리 상단의 필터(패키지 관리) 아이콘을 클릭해도 아무런 반응이 없는 것처럼 보이는 현상.
- **원인**:
1. `selector.js`에서 모달 팝업 생성 시 `modal.style.display = 'flex'`만 사용함.
2. 공통 CSS(`modal.css`)에서 `.modal-overlay`는 기본적으로 `opacity: 0`이며 `.active` 클래스가 있어야만 화면에 표시되도록 정의되어 있어 시각적으로 숨겨진 상태였음.
3. 구형 코드인 `static/js/modules/assets.js` 파일이 잔류하여 디버깅 및 유지보수에 혼선을 줌.
- **조치**:
1. `selector.js`의 모달 제어 로직을 `classList.add('active')``remove('active')` 방식으로 변경하여 CSS 애니메이션 및 가시성 확보.
2. 사용되지 않는 구형 `assets.js` 파일을 삭제하여 모듈 구조 단순화.
### [Instance #2275] - Selection Highlight Disappears during Edit
- **현상**: 선(edge) 속성창에서 시작/대상 호버 하이라이트가 표시된 상태에서 색상이나 스타일을 변경하면 하이라이트가 즉시 사라짐.
- **원인**: `renderProperties()` 시작 부분에 `unhighlight()`가 포함되어 있어, 속성 변경에 따른 리렌더링 발생 시마다 하이라이트가 강제로 초기화됨.
- **조치**:
1. `renderProperties()` 내부의 상시 `unhighlight()` 호출 제거.
2. 선택된 객체가 변경될 때 발생하는 `selection:changed` 이벤트 핸들러에서만 `unhighlight()`를 호출하도록 로직 위치 변경.
3. 이를 통해 동일 객체의 속성을 수정하는 동안에는 시각적 하이라이트가 유지되도록 정합성 확보.
### [Instance #2270] - Infinite Loop in Sidebar Rendering (Selection Loop)
- **현상**: 선(edge) 선택 시 브라우저 콘솔에 `renderProperties start` 로그가 무한히 출력되며 성능이 저하되는 현상.
- **원인**:
1. 선 선택 시 호출되는 `unhighlight()`가 노드의 `attr`을 수정함.
2. `attr` 수정이 `cell:change:attrs` 이벤트를 발생시킴.
3. 해당 이벤트 리스너가 다시 `renderProperties()`를 호출하여 무한 루프가 발생함.
- **조치**:
1. `unhighlight``highlight` 호출 시 모든 속성 변경에 `{ silent: true }` 옵션을 부여하여 불필요한 이벤트 전파 차단.
2. `cell:change` 이벤트 리스너에서 `options.silent` 플래그를 확인하여 내부적인 시각 효과 변경 시에는 리렌더링을 건너뛰도록 개선.
### [Instance #2265] - Sticky Source/Target Highlights in Sidebar
- **현상**: 엣지 속성창에서 시작/대상 선택 상자에 마우스를 올렸을 때 나타나는 노드 강조(테두리)가 사이드바를 닫거나 선택을 바꿔도 사라지지 않고 유지됨.
- **원인**: 하이라이트 제거 로직(`unhighlight`)이 오직 `mouseleave` 이벤트에만 바인딩되어 있어, 사이드바를 닫거나 다른 객체로 전환 시 이벤트가 트리거되지 않음.
- **조치**:
1. `unhighlight` 로직을 모듈 레벨 함수로 분리.
2. 사이드바 닫기 버튼, 적용 버튼 클릭 시 및 속성창 리렌더링(`renderProperties`) 시작 시점에 항상 `unhighlight`를 호출하도록 보강하여 잔상 방지.
### [Instance #2260] - Edge Rendered Underneath Groups (Z-Index Hierarchy)
- **현상**: 새로 생성된 선(edge)이 기존 그룹의 배경 뒤로 숨어서 보이지 않거나 선택이 어려운 현상.
- **원인**: 선 생성 시 레이어 오프셋이 적용되지 않은 기본 z-index(30)가 부여되어, 이미 레이어 가중치(101, 201 등)를 받은 그룹보다 낮게 배치됨.
- **조치**:
1. `layers.js`에 중앙 집중형 z-index 계산 함수(`calculateCellZIndex`)를 도입하여 레이어 오프셋과 객체 타입별 우선순위(노드>선>그룹)를 통합 관리.
2. 선 및 노드 생성 시(`config.js`, 스타일 매핑 모듈) 즉시 해당 함수를 호출하여 초기 z-index를 올바르게 할당하도록 구조 개선.
---
*아카이브 관리자: drawNET AI Assistant*
+155
View File
@@ -0,0 +1,155 @@
# drawNET 업데이트 로그 (Changelog)
## [v1.0 Alpha-1] - 2026-03-21
### 🎉 Project Milestone: Release Alpha Version
오늘로써 drawNET의 모든 핵심 엔진 기능이 구현되어 공식 **v1.0 Alpha-1** 버전으로 명명합니다.
## [2026-03-21] - High-Fidelity 하이브리드 내보내기 슈트 및 안정성 고도화
### 🚀 신규 기능 (Added)
- **엣지 라우팅 및 앵커 가시성 제어 거버넌스 수립**:
- **시작/대상(S/T) 가이드 라벨**: 엣지 선택 시 양 끝에 초록/빨강 라벨을 표시하여 망 구성 식별력 강화.
- **인터랙티브 앵커 하이라이트**: 사이드바 앵커 설정 시 캔버스 내 노드 실시간 점멸 피드백 제공.
- **직선 라우팅 최우선 순위화**: 앵커 방향 제약 및 전역 기본 설정을 극복한 '완벽한 직선' 모드 구현.
- **Format Painter (Style Copy/Paste) 엣지 확장**:
- `Ctrl+Shift+C/V`를 통해 선의 색상, 스타일, 라우팅, 화살표 정보를 다른 선으로 즉시 복제.
- **UX 편의 기능**:
- **ESC 전역 캔슬**: 원터치로 모든 선택 해제 및 사이드바 닫기.
- **단축키 도움말(?) 애니메이션 복구**: 누락된 리소스를 보강하여 도움말 모달 가독성 개선.
- **객체 메타데이터 및 인벤토리 거버넌스**:
- **상세 설명(Description) 및 태그(Tags) 도입**: 모든 노드와 엣지에 설계 의도 및 기술 사양을 기록할 수 있는 전용 필드 추가.
- **3중 데이터 무결성 보장 로직**: 구버전 파일 자동 마이그레이션, 복사/붙여넣기(`Ctrl+C/V`) 시 전역 데이터 새니타이저(Sanitizer)를 통한 필수 필드 누락 원천 봉쇄.
- **엔터프라이즈급 PPTX 리포팅 고도화**:
- **상세 설명 전용 슬라이드**: `autoPage` 기능을 활용하여 대규모 프로젝트의 모든 객체 상세 주석을 표 형태로 자동 생성.
- **인벤토리 매핑 강화**: 상세 목록에 '소속 그룹(Parent Group)' 및 '태그' 컬럼을 추가하여 외부 자산 데이터와의 매칭 편의성 제고.
- **High-Fidelity 하이브리드 내보내기 슈트 (v2.0)**:
- **SVG 내보내기 엔진 완전 재설계**: 전수 이미지 스캔 로직을 통해 상속된 아이콘까지 100% Base64로 임베딩하여 '아이콘 깨짐' 현상 종결.
- **절대 좌표 기반 짤림 방지 (Zero-Clip)**: `getCellsBBox`와 명시적 `viewBox` 주입을 통해 화면 줌/위치와 무관하게 120px 안전 여백이 포함된 완벽한 도면 출력.
- **전문가용 내보내기 UI**: 내보내기 프로세스 시작 시 "파일 준비 중" 안내 오버레이와 블러 효과를 적용하여 사용자 피드백 및 경험(UX) 고도화.
- **적응형 안정화 지연 (1000ms)**: 모델 업데이트 후 DOM 동기화를 위해 1초의 대기 시간을 부여하여 저사양 PC에서도 무결한 캡처 보장.
- **전문가용 PPTX 리포트**: 도면은 고해상도 PNG 스냅샷으로, 데이터는 편집 가능한 네이티브 표(Table)로 구성하는 하이브리드 모델 도입.
- **레이어별 자동 슬라이드 생성**: 멀티 레이어 프로젝트를 분석하여 레이어별 상세 도면 슬라이드를 자동으로 생성하는 엔진 구축.
- **초정밀 클린 캡처 (Clean Capture)**:
- 내보내기 시 앵커(포트), 선택 박스, UI 가이드를 자동으로 숨겨 시각적 노이즈를 제거하는 로직 반영.
- **2x 해상도 렌더링**: 모든 이미지 기반 내보내기 시 200% 스케일업을 통해 레티나 디스플레이 및 대형 인쇄 대응 수준의 화질 확보.
- **사이드바 데이터 정합성 및 실시간 동기화 슈트**:
- **재귀적 데이터 전파(Recursive Poke)**: 그룹 이름 변경 시 하위 모든 객체들을 대상으로 동기화 이벤트를 강제 트리거하여 사이드바의 상위 그룹 정보가 실시간으로 갱신되도록 개선 (#2245).
- **세이프 렌더(Safe Render) 및 포커스 유실 방지**: 사이드바 입력 중 그래프 변화에 의한 리렌더링을 지능적으로 차단하여 타이핑 연속성 확보 (#2255).
- **비동기 데이터 렌더링 타이즈**: `setTimeout(0)`을 적용하여 X6 엔진 데이터와 UI 간의 미세한 타이밍 불일치 완전 소거.
- **레이어 격리 및 상호작용 보안 거버넌스**:
- **선택 필터링(Selection Filter)**: 활성 레이어가 아닌 객체는 마우스로 선택할 수 없도록 플러그인 레벨에서 격리 구현.
- **상호작용 가드**: 비활성 레이어 객체에 대한 더블 클릭 등 파괴적 액션을 원천 차단하여 레이어 간 데이터 오염 방지.
- **개발 생산성 도구**:
- **`logger.debug` 정식 지원**: 개발 중 임시 로그를 위한 별도의 디버그 레벨과 서식 도입.
- **하이엔드 레이어 안정성 및 데이터 거버넌스 수립**:
- **논리 전용 레이어(Logical-Only Layer)**: 물리 배치와 논리 연결의 명확한 분리를 위한 레이어 타입 규격 도입 및 지능형 드롭/복제 필터링 엔진 구축.
- **레이어 하드-락(Hard-Lock) 시스템**: 중요 레이어의 실수에 의한 삭제나 이름 변경을 원천 차단하는 자물쇠 기능 도입.
- **데이터 보호 이중 확인(Double Confirmation)**: 오브젝트가 포함된 레이어 삭제 시 '항목 수 안내'와 '최종 파기 확인'의 2단계 절차를 거치는 안전 로직 적용.
- **레이어 전용 휴지통(Trash Bin)**: 드래그 앤 드롭으로 레이어를 명시적으로 버릴 수 있는 직관적인 폐기 워크플로우 구현.
- **레이어 패널 UI 최적화**: 가시성과 조작 편의성 개선을 위해 레이어 관리창 전체 크기를 약 25% 확대 (250px x 400px).
- **멀티 플랫폼 및 브라우저 호환성 하모니**:
- **Mac Command(⌘) 키 완벽 지원**: Mac 환경에서도 Cmd 키를 사용해 복제 및 단축키 기능을 동일하게 사용 가능.
- **자기 치유형 키 상태 관리(Self-healing)**: 탭 전환 등으로 인한 키 업/다운 이벤트 유실 시에도 마우스 활동 감지를 통해 시스템 키 상태를 자동 보정.
- **ESC 전역 캔슬 확장**: 드래그/복제 도중 `Escape` 키로 즉시 취소 및 고스트 박스 자동 소거 로직 고도화.
- **포맷 페인터(Format Painter) 데이터 무결성 수정**:
- 선(Edge)의 색상 및 두께 복사 시 UI 기본값보다 클립보드 데이터를 최우선(Strict Prioritization)으로 하여 속성이 누락되던 현상 해결.
- **하이엔드 안정성 및 논리 레이어 정책 거버넌스 수립**:
- **논리 전용 레이어(Logical-Only Layer)**: 물리 배치와 논리 연결의 명확한 분리를 위한 레이어 타입 규격 도입 및 지능형 드롭/복제 필터링 엔진 구축.
- **멀티 플랫폼(Windows/Mac) 하모니**: Cmd/Ctrl 키 통합 지원 및 자기 치유형(Self-healing) 키 상태 관리 로직 적용.
- **ESC 드래그 캔슬**: 복제 드래그 중 Escape 키로 즉석 취소 및 고스트 박스 자동 소거 로직 반영.
### 🛠️ 구조 개선 (Refactored)
- **내보내기 서브시스템(Export Subsystem) 모듈화**: `image_exporter.js`, `pptx_exporter.js` 등으로 로직을 분리하여 확장성 강화.
- **UI 시각적 가시성 개선**: 라이트 모드(White Mode)에서 플라이아웃 메뉴의 호버 대비(Contrast)를 액센트 컬러로 교체하여 접근성 및 전문성 강화.
- **중복 초기화 제거 (Zero Duplication)**: `app.js` 내로 핵심 모듈 초기화 로직을 일원화하여 중복 팝업 및 이벤트 리스너 누수 문제 원천 해결.
### 🐛 버그 수정 (Fixed)
- **레이어 데이터 손실 방지**: 새로고침 시 레이어 정보가 유실되던 데이터 영속성 결함 수정.
- **아이콘 매핑 정확도 향상**: `assetId` 기반의 정밀 매핑 및 경로 복제 방지 로직을 통해 PPTX 내 아이콘 404 오류 해결.
- **그룹 노드 렌더링 정상화**: PPTX 내에서 그룹 객체가 거대한 이미지로 보이던 현상을 투명도를 가진 네이티브 사각형으로 수정.
### ✅ 완료된 마일스톤
- **Phase 1.50**: High-Fidelity 하이브리드 내보내기 거버넌스 수립 완료
- **Phase 1.55**: 시스템 초기화 및 안정성 전수 감사(Full Audit) 완료
- **Phase 1.60**: 로컬 독립 실행 및 오프라인 리포팅 환경 구축 완료
---
## [2026-03-20] - i18n 거버넌스 및 아키텍처 전략 고도화
### 🚀 신규 기능 (Added)
- **전역 다국어(i18n) 통합 지원 (ko/en)**:
- 브라우저 언어 설정 및 사용자 선택에 대응하는 실시간 로케일 전환 엔진 구축.
- `en.json` 신설을 통한 영미권 사용자 지원 기반 마련.
- **기술 용어 직관화 (Terminology Optimization)**:
- 난해한 공학 용어를 직관적인 한국어로 교체: `Manhattan` -> `자동 경로 (장애물 회피)`, `U-Shape` -> `ㄷ자 경로`, `Orthogonal` -> `수직/수평 (일반)`.
- **PPTX 아키텍처 리포트 다국어화**:
- 생성되는 모든 PPTX 보고서의 헤더, 요약 표, 인벤토리 항목을 설정 언어에 맞춰 동적 렌더링하도록 개선.
### 🛠️ 구조 개선 (Refactored)
- **하드코딩 문자열 제로화 (Hardcoding-Free)**:
- `Object Studio`, `Settings`, `Persistence`, `UI Templates` 등 시스템 전반의 텍스트를 로케일 파일로 분리 관리.
- **아키텍처 문서(`architecture.md`) 대대적 보강**:
- **Section 4 (Multi-Layer Isolation)**: 물리/논리/보안 레이어 간 SSOT(Single Source of Truth) 보장을 위한 데이터 평면 설계 명문화.
- **Section 5 (PM Assets & Expansion)**: 외부 자산 연동 및 REST API 확장 전략 구체화.
- **Section 8 (AI-Ready Strategy)**: 정규화된 JSON 구조를 통한 AI 분석 및 네트워크 시뮬레이션 가치 강조.
- **로케일 데이터 무결성 확보**: JSON 내 중복 키 전수 제거 및 정적 분석 오류 해결.
### 🚀 기존 Premium UX 업데이트 내역 (이전 작업)
- **오브젝트 적층 순서 제어 (Z-Index Control)**: `[` (뒤로 보내기), `]` (앞으로 가져오기) 단축키 지원.
- **오브젝트 정밀 잠금 (Full Object Locking)**: `Ctrl + L` 및 시각적 피드백(Red Dashed Boundary) 구현.
- **재귀적 오브젝트 선택 (Recursive Selection)**: `Ctrl + 마우스 우클릭`을 통한 Surgical Picker 도입.
- **프리미엄 레이어 패널**: 레이어 이름 변경 및 비활성 레이어 투명도 조절 기능.
### ✅ 완료된 마일스톤
- **Phase 1.30**: 전역 다국어 통합 거버넌스 및 하드코딩 제거 완료
- **Phase 1.35**: 아키텍처 설계서(architecture.md) 엔지니어링 표준화 완료
- **Phase 1.40**: UI/UX 용어 고도화 및 전문가 DX 공식 수립
---
## [2026-03-19] - Object Studio & Asset Infrastructure 고도화
### 🚀 신규 기능 (Added)
- **Object Studio 전용 페이지 (`/studio`)**: 독립된 환경에서 자산을 제작하고 관리할 수 있는 전문 도구 세트 구현.
- **하이브리드 벡터 변환 엔진**: Potrace-js를 통합하여 PNG 비트맵 이미지를 고품질 SVG `<path>` 데이터로 실시간 변환하는 기능 추가.
- **멀티 벤더 패키징 시스템**:
- 에셋을 벤더 단위 팩(`package.json`)으로 묶어 물리적 디렉토리에 저장하는 서버 API 구현.
- 저장된 패키지가 설계 영역(Designer) 라이브러리에 실시간 로드되는 인프라 구축.
- **인제스터(Ingester) 개선**: 폴더 단위 업로드(`webkitdirectory`) 및 드래그 앤 드롭을 통한 대량 자산 일괄 로드 지원.
- **리뷰 및 선택 UI**: 원본과 변환본을 나란히 비교하고, 사용자 취향에 따라 포맷을 선택할 수 있는 Side-by-Side 뷰 도입.
### 🛠️ 구조 개선 (Refactored)
- **스튜디오 프론트엔드 모듈화**: `Renderer`, `Actions`, `State`, `Processor` 등으로 로직을 분리하여 확장성 및 유지보수성 확보.
- **백엔드 자산 스캔 로직 최적화**: `api_routes.py` 내 멀티 패키지 로딩 엔진 고도화.
- **명명 규칙 자동화**: 파일명을 기반으로 슬러그(Slug) 형태의 ID와 라벨을 자동 생성하는 로직 반영.
### ✅ 완료된 마일스톤
- **Phase 1**: 데이터 규격 및 멀티 로더 (Vendor Pack 시스템)
- **Phase 2**: 스튜디오 기반 구축 (라우팅, 레이아웃, 인제스천)
- **Phase 3**: 핵심 가공 기능 (SVG 트레이싱, 비교 검수)
- **Phase 4**: 패키징 및 설계영역 최종 연동 (Build & Save)
---
## [2026-03-18] - UX & Precision Tools 고도화
### 🚀 신규 기능
- **단축키 UX 고도화**: 컨텍스트 메뉴 내 단축키 힌트 노출 및 `Alt + 1` 사이드바 토글 등 전문 도구 수준의 단축키 전략 확립.
- **신규 객체 '리치 텍스트 카드(Rich Text Card)' 출시**:
- 아키텍처 도면의 '구축 내역' 및 '범례(Legend)'를 위한 전문 문서화 객체 추가.
- 지능형 리스트 렌더링 (번호 사각형, 불렛 기호 자동 생성).
- 범례 모드 지원: `- (도형:색상) 설명` 텍스트 기반 시각화 자동화.
- **Format Painter (Style Copy/Paste) 엣지 확장**: `Ctrl+Shift+C/V`를 통해 노드뿐만 아니라 선(Edge)의 라우팅, 색상, 스타일을 일괄 복사/적용 가능.
- **ESC 키 전역 캔슬**: `Esc` 키로 선택 해제 및 사이드바 닫기 지원.
- **다중 선택 정렬 패널**: 여러 노드 선택 시 사이드바에 즉시 정렬 및 스타일 편집 옵션 표시.
### 🐛 버그 수정
- **캔버스 팬닝 충돌 해결**: Space 키 드래그 시 Rubberband 선택과 겹치는 현상 수정 (인터랙션 명시적 비활성화).
---
## [이전 로그] - 프로젝트 초기화 및 핵심 엔진 구축
- AntV X6 그래프 엔진 기반 마이그레이션 완료.
- DSL(Domain Specific Language) 파싱 및 캔버스 동기화 인프라 구축.
- 그룹화, 라우팅, 인벤토리 등 핵심 기능 순차적 도입.
+79
View File
@@ -0,0 +1,79 @@
# drawNET 기능 명세서 (v1.0 Alpha)
현재 구현된 주요 기능들의 목록과 상세 설명입니다. "이 문서에 없는 기능은 구현되지 않은 것으로 간주합니다."
## 1. 프로젝트 관리 (Project Management)
- **JSON 기반 SSoT**: `graph.toJSON()``graph.fromJSON()`을 사용하여 프로젝트 상태(노드, 엣지, 위치, 스타일 등)를 완벽하게 보존.
- **자동 저장 및 세션 복구**: `persistence.js`를 통해 변경 사항 발생 시 브라우저 LocalStorage에 실시간 자동 저장.
- **임포트/익스포트**: `.json` 또는 `.dnet` 확장자로 프로젝트 파일을 내보내고 가져오기 지원.
## 2. 그래프 캔버스 (Topology Canvas)
- **AntV X6 v2.x 엔진**: 엔터프라이즈 레벨의 고성능 그래프 렌더링.
- **UUID 아키텍처**: 모든 오브젝트에 고유 ID를 부여하여 라벨 중복과 무관하게 데이터 무결성 보장.
- **정밀 직교 라우팅 (Manhattan)**: 선 꼬임 방지 및 직각 연결 유지 알고리즘 적용.
- **스마트 앵커 (Smart Anchors)**: 상/하/좌/우 포트 고정을 통한 정교한 경로 제어.
## 3. 지능형 그룹화 (Smart Grouping)
- **컨테이너-자식 관계**: 드래그 앤 드롭을 통한 계층 구조 형성.
- **가변 패딩 조절**: 속성 사이드바를 통해 그룹 내부 여백 실시간 조정 가능.
- **UUID 계층 구조**: 부모-자식 관계가 UUID로 관리되어 노드 이동 및 이름 변경 시에도 계층 유지.
- **최하위 그룹 자동 인식 (Innermost Selection)**: 그룹이 중첩되거나 겹쳐 있는 경우, 시스템이 자동으로 가장 면적이 작은(가장 안쪽의) 그룹을 부모로 선택하여 정밀한 계층 관리 지원.
## 4. 사이드바 및 속성 제어 (Sidebar & Properties)
- **자산 라이브러리**: 카테고리별 자산 트리 및 실시간 검색 필터링.
- **상세 속성 편집**:
- 노드: 타입, 라벨, 부모(UUID), IP, 상태, 벤더, 커스텀 어셋 경로 등.
- 엣지: 타입, 스타일, 색상, 라우팅 방식, 앵커 위치, 터널링 여부 등.
- **자동 사이드바 오픈**: 오브젝트 선택 시 해당 속성 창 자동 노출.
## 5. 정밀 배치 및 협업 도구 (Precision Tools)
- **인벤토리 패널**: 타입별 자산 수량 실시간 집계 및 CSV 내보내기 지원 (`Alt + 0`).
- **서식 복사 (Format Painter)**: `Ctrl + Shift + C/V`를 통해 노드 및 엣지(선)의 스타일을 일괄 복제.
| 구분 | 대상 | 복사/붙여넣기 항목 |
| :--- | :--- | :--- |
| **노드** | 데이터 & 디자인 | 제조사, 모델, 상태, 프로젝트, 태그, 배경색, 글꼴 크기/색상 등 |
| **엣지** | 라우팅 & 스타일 | **직선/직각 모드**, 선 색상, 두께, 점선 여부, 시작/대상 앵커 위치 등 |
- **오브젝트 적층 제어 (Stacking Order)**: `[` (Send to Back), `]` (Bring to Front) 단축키를 사용하여 겹친 오브젝트의 상하 순서 조절.
- **물리적 객체 잠금 (Object Locking)**: `Ctrl + L` 또는 속성창의 스위치를 통해 이동, 삭제, 회전을 원천 차단. 잠긴 객체는 **빨간색 점선 경계**로 표시됨.
- **재귀적 오브젝트 선택 (Recursive Picker)**: `Ctrl + 마우스 우클릭` 시 마우스 포인터 위치에 중첩된 모든 오브젝트 목록을 팝업하여, 복잡한 레이어 구조에서 원하는 항목만 정밀하게 선택 가능.
- **비주얼 디프 (Visual Diff)**: JSON 스냅샷 비교를 통해 이전 상태 대비 변경된 위치 및 추가된 노드 하이라이트.
- **드래그 복제**: `Ctrl + Drag`를 통한 즉시 노드 복제 (고스트 박스 기반 실시간 프리뷰).
- **하이엔드 안정성(Failsafe) 설계**:
- **ESC 즉시 취소**: 드래그 또는 복제 중 ESC 키 입력 시 모든 상태 초기화 및 고스트 제거.
- **자기 치유형 키 상태(Self-healing)**: 브라우저 탭 전환 등으로 인한 키 업/다운 이벤트 유실 시, 마우스 활동 감지로 실제 키 상태 자동 재동기화.
- **멀티 플랫폼(Mac) 지원**: Windows(Ctrl)와 Mac(Command) 키를 통합 지원하여 동일한 복제 워크플로우 제공.
## 6. 오브젝트 스튜디오 (Object Studio)
- **대량 이미지 업로드**: 드래그 앤 드롭으로 다량의 에셋 일괄 처리.
- **PNG to SVG 벡터 변환**: Potrace-js 엔진을 활용한 고품질 벡터화.
- **실시간 비교 뷰어**: 비트맵과 벡터 결과물의 실시간 품질 대조.
- **벤더 패키징**: 가공된 에셋을 라이브러리로 즉시 패키징하여 반영.
## 7. 단축키 시스템 (Hotkeys - Premium)
- **Alt + 0/1**: 인벤토리 / 속성 사이드바 토글.
- **Alt + 9**: 레이어 패널 토글.
- **Shift + A/D**: 빠른 선 연결 / 끊기.
- **Shift + 1~6**: 오브젝트 자동 정렬 (Alignment).
- **Shift + 7~8**: 오브젝트 균등 분배 (Distribution).
- **F2**: 선택된 오브젝트의 라벨 즉시 편집.
- **Directional Keys**: 5px / 그리드 단위(Shift) 정밀 이동.
## 8. 논리적 계층화 (Multi-Layer Support)
- **레이어 기반 가시성 제어**: 무제한 논리적 레이어 생성 및 개별 Visible 토글.
- **비활성 레이어 투명도 (Opacity Control)**: 슬라이더를 통해 배경 레이어의 투명도를 0~100%로 조절하여 집중도 향상.
- **레이어 리네임 (Rename)**: 편집 아이콘(Pencil)을 사용하여 레이어 이름을 직관적으로 변경하고 영속 저장.
- **액티브 레이어 자동 할당**: 현재 선택된 레이어에 신규 오브젝트 자동 귀속.
- **논리 전용 레이어 (Logical-Only Layer)**:
- 레이어별로 **'장비(Physical)'** 또는 **'연결(Logical)'** 성격 부여 가능.
- 논리 레이어에서는 **노드 드롭 차단****지능형 노드 필터링(복제 시 선만 추출)** 기능을 통해 설계 실수 방지 및 시뮬레이션 효율 극대화.
- **지능형 적층 계층 (Z-Index Hierarchy)**: `Group (1) < Edge (30) < Node (50)` 계층을 기본으로 하여, 연결선이 그룹에 가려지지 않고 항상 인터랙션 가능하도록 최적화.
## 9. 시스템 데이터 무결성 (Data Integrity & Recovery)
- **자동 데이터 자가 치유(Self-healing)**: 프로젝트 로드 시 유실된 좌표(`pos`), 크기(`size`), 라벨(`label`) 등을 `data` 필드로부터 추론하여 자동으로 복구 및 동기화.
- **명시적 저장 모델 (Explicit Property Apply)**: 사이드바 텍스트 수정 시 실시간 반영 대신 **[적용]** 버튼이나 **Enter** 키를 통한 명시적 확정 방식을 채택하여, 입력 도중의 포커스 유실을 방지하고 데이터 경합 최소화.
- **명시적 연결선 메타데이터 (Explicit Edge Layers)**:
- 모든 연결선에 `source_layer`, `target_layer`, `is_cross_layer` 정보를 명시적으로 기록.
- 이를 통해 전체 도면 데이터 없이도 연결성(Connectivity)을 독립적으로 해석 가능하게 하며, 외부 도구 및 LLM 분석 시의 정밀도를 대폭 향상.
---
- **마지막 업데이트**: 2026-03-22 (명시적 연결선 메타데이터 추가, Z-Index 계층화, 데이터 자가치유 로직)
+90
View File
@@ -0,0 +1,90 @@
# drawNET Maintenance Guide (AI 개발자 필독)
이 문서는 drawNET 프로젝트의 연속성을 유지하기 위한 **핵심 기술 규칙 및 유지보수 지침**을 담고 있습니다. 새로운 세션의 AI 개발자는 작업을 시작하기 전 본 문서를 반드시 숙지하여 기존의 설계 철학이 깨지지 않도록 해야 합니다.
---
## ⌨️ 3. 단축키 시스템 (Hotkeys)
- **규칙**:
1. `static/hotkeys.json`에서 `undo``redo` 액션은 반드시 **`"alwaysEnabled": true`** 속성을 가져야 합니다.
2. `static/js/modules/hotkeys.js``isTyping` 상태(텍스트 입력 중)일지라도 `alwaysEnabled`가 참인 단축키는 차단하지 않고 실행해야 합니다.
3. **이유**: 예기치 않게 특정 입력창에 포커스가 머물러 있더라도 그래프의 Undo/Redo 기능은 항상 작동해야 하기 때문입니다.
## 🎨 4. 오브젝트 스튜디오 (Studio)
- **규칙**:
1. **ID 고유성**: `static/js/modules/studio/state.js``addSource(file)` 함수는 파일명이 중복될 경우(예: `icon.png`, `icon.png`) `id` 뒤에 `_1`, `_2` 등의 접미사를 붙여 반드시 고유한 ID를 생성해야 합니다.
2. **ID 생성 방식**: 소문자 영문과 숫자 위주로 변환하며, 공백이나 특수문자는 언더바(`_`)로 치환합니다.
## 📂 5. 에셋 라이브러리 (Asset Library)
- **구조**: `Pack (Package) -> Category -> Object` 3단계 계층 구조를 유지합니다.
- **필터링**: 사용자가 사이드바의 **'Filter' 아이콘**을 통해 원하는 패키지만 선택적으로 로드할 수 있어야 하며, 이 설정은 `localStorage``selectedPackIds`에 저장됩니다.
- **Composite ID (중요)**: 에셋의 `id`가 패키지 간에 중복될 수 있으므로, `state.assetMap`의 키와 드래그 앤 드롭 통신에는 반드시 **`${pack_id}|${asset.id}`** 형식의 조합 키를 사용해야 합니다.
## 🧼 6. 오브젝트 데이터 정합성 (Object Data Purity)
- **규칙**:
1. **그룹 및 기본 도형(Primitives)**: `group`, `rect`, `circle`, `rounded-rect`, `text-box`, `label` 등 시각적 도형 객체는 에셋 이미지 경로(`assetPath`)를 가져서는 안 됩니다.
2. **ID 매칭 주의**: `static/js/modules/graph/styles/mapping.js``getX6NodeConfig` 함수는 객체 타입이 그룹이거나 기본 도형 목록에 포함될 경우 에셋 검색 및 할당 로직을 반드시 건너뛰어야 합니다.
3. **라벨 데이터-시각 속성 동기화**: `data.label`은 사이드바와 비즈니스 로직의 원본 소스(SSoT)입니다. `json_handler.js``handleNodeUpdate`는 항상 `data.label`의 값을 `attrs.label.text`에 강제로 동기화하여 시각적 불일치를 방지해야 합니다.
4. **이유**: 그룹 이름이나 도형 라벨이 기존 에셋 명칭을 포함할 경우(예: "Switch Group"), 시스템이 이를 에셋으로 오판하여 잘못된 아이콘을 렌더링하거나 404 에러를 유발할 수 있기 때문입니다.
## 🔄 7. 부모 그룹 선택 가변 로직 (Parent Selection Heuristic)
- **원칙**: 노드가 중첩되거나 겹쳐 있는 여러 그룹 영역에 드롭/이동될 경우, 시스템은 항상 **가장 구체적인(Innermost)** 부모를 선택해야 합니다.
- **판단 알고리즘**:
1. **면적 기반 우선순위**: 드롭 포인트를 포함하는 모든 그룹 후보 중 Bounding Box의 **면적(Area)이 가장 작은 그룹**을 최종 부모로 선택합니다.
2. **계층 기반 보완**: 만약 후보 그룹들의 면적이 동일한 경우(예: 기본 사이즈로 중첩된 경우), `getAncestors()`를 확인하여 다른 그룹의 **자손(Descendant)인 그룹**을 우선적으로 선택합니다.
- **적용 위치**: `static/js/modules/graph/config.js` (`embedding.findParent`) 및 `static/js/modules/graph/interactions/drop.js`.
## 🧩 8. 모듈화 및 리팩토링 규칙 (Refactoring & Import Integrity)
- **규칙**:
1. **상대 경로 재계산**: 파일을 하위 디렉토리로 이동(예: `events.js``events/index.js`)할 경우, 해당 파일 내의 모든 상대 경로 임포트(`import ... from '../../state.js'`)는 반드시 새로운 디렉토리 깊이에 맞춰 업데이트해야 합니다.
2. **공통 참조 체크리스트**:
- `state.js`, `constants.js` (보통 `modules/` 최상위)
- `graph/styles/utils.js` 등 공용 유틸리티
- `ui/`, `persistence.js` 등 외부 모듈 의존성
3. **검증 절차**: 모듈화 작업 직후에는 반드시 `grep`이나 IDE 기능을 사용하여 **"지정된 파일을 찾을 수 없습니다"** 또는 **"404 Not Found"** 오류가 없는지 전수 검사해야 합니다.
- **이유**: 파일 구조가 깊어짐에 따라 기존의 `./` 또는 `../` 경로가 깨지면서 런타임 오류를 유발하고 개발 흐름을 방해하기 때문입니다.
## 11. 사이드바 및 실시간 동기화 (Sidebar & Sync)
- **규칙**:
1. **세이프 렌더(Safe Render)**: 사이드바의 `input` 또는 `textarea`에 포커스가 있는 동안에는 그래프 이벤트에 의한 전체 리렌더링을 차단해야 합니다. (`index.js``safeRender` 함수 참조)
2. **비동기 렌더링 튜닝**: X6의 데이터 변경 이벤트(`cell:change:data`) 발생 직후 렌더링할 때는 반드시 `setTimeout(..., 0)`을 사용하여 엔진 내부의 데이터 확정이 완료된 후 UI를 그려야 합니다.
3. **명시적 저장(Explicit Apply)**: 라벨, 자산 정보 등 텍스트 기반 속성은 `Enter` 키 또는 **[적용]** 버튼을 통해서만 저장되도록 하여, 입력 중 데이터 유실이나 포커스 탈취를 원천 차단합니다.
4. **재귀적 전파(Recursive Poke)**: 그룹의 이름이나 레이아웃 정보를 변경할 경우, 반드시 모든 하위 객체(`getDescendants`)에 `_parent_updated`와 같은 더미 타임스탬프를 `setData`로 주입하여 하위 객체들의 사이드바가 즉각 최신 부모 정보를 반영하게 해야 합니다.
- **이유**: 실시간 협업 및 복잡한 계층 구조에서 데이터 정합성과 사용자 입력의 연속성을 동시에 보장하기 위한 필수 장치입니다.
## 12. 레이어 격리 및 선택 필터링 (Layer Isolation)
- **규칙**:
1. **선택 격리(Selection Filter)**: `plugins.js``Selection` 플러그인 설정에서는 반드시 현재 활성 레이어(`state.activeLayerId`)에 속한 셀만 선택 가능하도록 필터링해야 합니다.
2. **상호작용 차단**: 활성 레이어가 아닌 객체에 대한 더블 클릭(더 하위 계층으로 진입 등)은 `nodes.js` 이벤트 핸들러에서 명시적으로 차단 가드를 두어야 합니다.
3. **정적 필터링**: `pointer-events: none`과 같은 CSS 기반 차단 대신, 플러그인 레벨의 필터링을 우선하여 고스트 스냅(Ghost Snapping) 등 보조 기능은 유지되도록 관리합니다.
- **이유**: 사용자가 편집 중이지 않은 레이어의 객체를 실수로 조작하여 데이터가 오염되는 것을 방지하기 위함입니다.
## 9. 오브젝트 적층 및 잠금 관리 (Stacking & Locking)
- **Z-Index 규칙 (2026-03-21 개정)**:
- **계층 구조**: `Group/Rack (1~19) < Edge (30) < Node (50)`
- 그룹은 하단에, 연결선(Edge)은 그룹 위에, 일반 노드는 최상단에 배치하여 가시성 및 클릭 편의성을 보장합니다.
- 사용자 조작(`[`, `]`)에 의한 동적 변경 시에도 이 기본 계층 범위를 크게 벗어나지 않도록 주의합니다.
- **잠금(Lock)의 물리적 강제**:
- 단순히 `data.locked` 필드만 변경하는 것이 아니라, `cell.setProp`을 통해 `movable`, `deletable`, `rotatable` 속성을 동기화해야 합니다.
- `static/js/modules/graph/config.js``interacting` 블록은 반드시 함수형으로 작성되어 각 셀의 `movable` 속성을 실시간으로 체크해야 합니다.
## 10. 정밀 공간 쿼리 (Spatial Query Integrity)
- **규칙**:
- 특정 좌표(`x, y`)에서 오브젝트를 검색할 때, AntV X6 v2의 실험적/변동성이 큰 API(`findModelsAtPoint` 등)에 의존하지 않습니다.
- 대신 `graph.getCells().filter(cell => cell.getBBox().containsPoint(local))`와 같은 **수동 기하학 필터링** 방식을 유지하여 버전 업데이트에 따른 오작동을 방지합니다.
- **좌표 변환**: 반드시 브라우저의 클라이언트 좌표를 `graph.clientToLocal(e.clientX, e.clientY)`를 통해 그래프 로컬 좌표로 먼저 변환한 후 계산에 사용해야 합니다.
---
*마지막 업데이트: 2026-03-20*
+86
View File
@@ -0,0 +1,86 @@
# drawNET 프로젝트 로드맵 및 개발 가이드
이 문서는 drawNET 프로젝트의 개발 원칙, 완료된 마일스톤, 그리고 향후 발전 방향을 정의합니다.
---
## 🛠️ 개발 원칙 (Development Principles)
1. **하드코딩 배제:** 모든 자산과 설정은 `assets.json` 등 외부 파일을 통해 관리합니다.
2. **로컬 최적화:** 설치와 실행이 간편해야 하며, 보안 및 폐쇄망 대응을 위해 핵심 라이브러리(AntV X6 등)를 로컬에서 관리합니다.
3. **사용자 경험 최선:** 텍스트(DSL)와 시각적 편집(GUI)의 장점을 결합한 하이드리브 워크플로우를 지향합니다.
4. **엔진 독립성:** 특정 엔진에 종속된 기능을 지양하되, 현재는 AntV X6의 성능을 극대화하여 구현합니다.
---
## 📅 완료된 마일스톤 (Milestones)
### 1. 전용 그래프 엔진 마이그레이션 (AntV X6)
- **엔진 전환:** Cytoscape.js에서 AntV X6로 전체 시스템 이관 완료.
- **직교 라우팅:** Manhattan 알고리즘을 통한 완벽한 90도 굴곡 선 지원.
- **쉐이프 커스터마이징:** 이미지 및 메타데이터를 포함한 전용 노드 타입 등록.
- **로컬화:** 외부 CDN 차단 및 ORB 이슈 해결을 위한 라이브러리 자산 로컬화.
- **방향성 고정 라우팅 (Top/Bottom/Left/Right):** 앵커 선택에 따라 해당 방향으로 선이 가장 먼저 뻗어나오도록 강제하는 지능형 Manhattan 라우팅 엔진 구현 완료.
### 2. 하이브리드 속성 및 인터랙션
- **속성 사이드바:** X6 셀 데이터와 실시간 연동되는 속성 편집 패널.
- **컨텍스트 메뉴:** 우클릭을 통한 퀵 액션 및 스타일 제어.
- **지능형 그룹화:** 컨테이너 드롭 및 노드 드래그 포함을 통한 계층 구조 자동 생성.
### 3. 영속성 및 환경 관리
- **Leaf-Only 복구:** 계층 구조의 좌표 뒤틀림을 방지하는 X6 최적화 좌표 복구 엔진.
- **전용 포맷(.dnet):** 메타데이터 헤더를 포함한 X6 JSON 구조 표준화.
- **캔버스 환경 저장:** 그리드 스타일, 스냅 설정, 줌 레벨 등 시큐리티 가시성 환경 보존.
### 4. 인벤토리 및 정밀 배치 시스템
- **Inventory UI:** 타입별 수량 집계 및 드래그 가능한 플로팅 패널 구현.
- **데이터 기반 단축키 엔진:** `hotkeys.json` 설정을 통한 유연한 키 바인딩 시스템 도입.
- **키보드 네비게이션:** 방향키 및 Shift 조합을 통한 그리드 단위 오브젝트 정밀 이동 기능 추가.
- **파서 고도화:** 커스텀 포트 이름 및 그룹 멤버십 안정성 강화.
### 5. 오브젝트 상세 커스터마이징 및 단축키 전략 (New)
- **라벨 속성 강화**: 라벨 색상(Color) 및 위치(Top/Bottom/Left/Right/Center)를 속성창에서 즉시 변경 가능한 UI 구현.
- **단축키 계통 확립**: `Alt`(패널), `Shift`(액션), `Ctrl`(시스템)로 구분된 전문적인 단축키 설계 원칙 문서화 및 `Alt + 1` 적용.
- **실행 취소/다시 실행**: `Ctrl + Z/Y`를 통한 무제한 작업 실행 취소 기능 활성화.
### 7. Premium UX & 정밀 제어 시스템 (New)
- **오브젝트 적층(Z-Index)**: `[` / `]` 단축키를 통한 가역적 레이어링 시스템 구축 완료.
- **물리적 잠금(Locking)**: 인터랙션 제한 및 시각적 피드백(Red Boundary)을 결합한 강력한 객체 보호 기능 구현.
- **재귀적 선택(Recursive Select)**: 겹친 오브젝트를 정밀하게 골라내는 Surgical Picker(Photoshop Style) 시스템 완성.
- **레이어 패널 고도화**: 비활성 레이어 투명도 조절, 레이어 리네임 및 영속성 동기화 완료.
---
## 🚀 향후 개발 우선순위 (Future Priorities)
### 💡 Current Focus: Interaction & Topology Intelligence
현재 기본적인 정밀 제어 도구가 완성됨에 따라, **망(Network)의 논리적 연속성**을 보장하는 지능형 인터랙션으로 전환 중입니다.
### 🎯 주요 목표
- [x] **정밀 고정 및 적층:** 오브젝트 잠금 및 Z-Index 제어 완결.
- [x] **정밀 선택 도구:** 복잡한 도면에서의 surgical selection 지원.
- [x] **고스트 타겟팅 (Ghost Targeting):** 레이어 간 관통 연결 시 하부 레이어 노드에 대한 자석 효과 및 안내선 제공.
- [x] **레이어 스패닝 (Spanning):** 여러 레이어에 걸쳐 있는 거대 오브젝트(Backbone 등)의 통합 관리 로직.
### Phase 2: 고급 시각화 및 UX (중기)
- [x] **선 교차(Cross) 점프:** 선이 겹칠 때 시각적으로 건너뛰는(Jump/Hop) 브릿지 효과.
- [x] **실행 취소/다시 실행:** X6 History 플러그인을 활용한 무제한 Undo/Redo.
### Phase 5: PM 자산 관리 및 인벤토리 (Inventory & Asset Management) [x]
- [x] **표준 속성 확장**: 제조사, 모델명, IP, 자산번호, 상태 등 PM용 표준 데이터 스키마 적용.
- [x] **지능형 검색 및 이동 (Search & Zoom)**: IP, 자산번호, 모델명 기반 검색 및 해당 노드로 즉시 포커싱.
- [x] **속성 복사/붙여넣기 (Format Painter)**: `Ctrl+Shift+C/V`를 통한 스타일 및 관리 데이터 일괄 복제.
- [x] **BOM 인벤토리 내보내기 (Excel Export)**: 전체 자산 리스트를 엑셀/CSV로 자동 생성 및 다운로드.
- [x] **드래그 복제 (Ctrl+Shift+Drag)**: 마우스 드래그를 이용한 즉시 오브젝트 복제 구현.
---
### Phase 7: 멀티 레이어 아키텍처 및 논리적 계층화 (Multi-Layer & Logical Stratification)
- [x] **평면 중첩(Flat Overlay) 시스템**: 여러 망(내부, 외부, 폐쇄망)을 독립된 레이어로 분리하고 투명도 기반으로 겹쳐 보기 기능 구현.
- [x] **레이어 관통 스냅(Ghost Snapping)**: 다른 레이어의 노드(방화벽 등)에 자석 효과를 적용하여 레이어 간 연결 편의성 극대화.
- [ ] **입체 레이어 뷰 (3D Exploded View) [취소/폐기]**: 프레젠테이션용 아코디언 익스플로드 뷰 실험 결과 가독성 저하로 폐기 결정.
- [x] **디자인 가이드라인 수립**: [Multi-Layer 설계 지침](file:///c:/project/drawNET/docs/design_multilayer.md) 기반의 데이터 정합성 엔진 개발.
### Phase 4: 인프라 연동 및 자동화 (Integration & Automation)
- [x] **Export 다변화**: SVG, PDF, 고해상도 PNG(투명 배경) 및 전문적인 PPTX 리포트 자동 생성 기능.
+46
View File
@@ -0,0 +1,46 @@
## 1. 데이터 규격 정의 (package.json)
- [x] 패키지 기본 정보(ID, Vendor, Version) 필드 정의 완료
- [x] 에셋 개별 정보(ID, Label, Category, Paths) 필드 정의 완료
- [x] 전/후면(Front/Back) 뷰 확장을 고려한 데이터 구조 검토 완료
### [최종 확정 규격]
```json
{
"id": "cisco_nexus_9k",
"name": "Cisco Nexus 9000 Series",
"vendor": "Cisco",
"version": "1.0.0",
"description": "Standard Nexus switches for Data Center",
"assets": [
{
"id": "n9k_93180yc",
"label": "Nexus 93180YC-EX",
"category": "Switch",
"views": {
"icon": "icon.svg",
"front": "front.svg",
"back": "back.svg"
},
"specs": {
"u_height": 1,
"is_rack_unit": true
}
}
]
}
```
## 2. 설계영역(Draw) 패키지 로더 구현
- [x] `assets.js`가 단일 경로가 아닌 `packs/` 내의 모든 하위 패키지를 스캔하도록 수정
- [x] 사이드바 라이브러리 영역에 패키지별 섹션(또는 그룹) UI 생성
- [x] 개별 에셋을 드래그하여 캔버스 드롭 시 정상 렌더링 확인
## 3. 샘플 패키지 연동 테스트
- [x] `static/assets/packs/sample_cisco/` 수동 생성 및 테스트 데이터 수납
- [x] 설계영역 재로드 시 샘플 패키지가 라이브러리에 자동 노출되는지 확인
- [x] 언어(ko/en) 파일에 패키지 관련 신규 키 추가 여부 확인
## 4. 안정성 및 예외 처리
- [x] 잘못된 형식의 `package.json` 로드 시 에러 핸들링 및 사용자 알림
- [x] 에셋 파일 경로 누락 시 기본(Default) 아이콘 표시 로직 확인
- [x] 기존 라이브러리(Fixed Objects 등)와의 UI 충돌 여부 확인
+26
View File
@@ -0,0 +1,26 @@
# Phase 2 체크리스트: 스튜디오 기반 구축 (Base Setup)
## 1. 전용 페이지 및 라우팅 설정
- [x] `routes/main_routes.py``/studio` 엔드포인트 추가 완료
- [x] `templates/studio.html` 기본 구조(HTML5, 필요한 CSS/JS 로드) 생성 완료
- [x] 설계영역(Draw)에서 스튜디오로 이동할 수 있는 메뉴/버튼 배치 (예: 상단 툴바)
## 2. 스튜디오 전용 UI/UX 레이아웃 (3-Panel)
- [x] **Source Panel (좌측)**: 업로드된 파일 목록 및 Drag & Drop 영역 구현
- [x] **Workspace (중앙)**: 에셋들을 바둑판식(Grid)으로 보여주는 프리뷰 영역 구현
- [x] **Property Panel (우측)**: 선택된 에셋의 상세 정보(ID, Label) 입력 영역 스켈레톤 생성
- [x] 스튜디오 전용 다크 테마 및 전문 도구 스타일 CSS 적용
## 3. Ingestion(이미지 수집) 엔진 구현
- [x] 브라우저 폴더 선택(`webkitdirectory`) 기능 구현 및 파일 필터링(.svg, .png, .jpg)
- [x] 개별 파일 드래그 앤 드롭(Drag & Drop) 업로드 지원
- [x] 선택된 이미지들을 중앙 Workspace 그리드에 즉시 렌더링 (Memory URL 사용)
## 4. 선택 및 상태 서비스 (State Management)
- [x] 그리드 내 이미지 개별/다중 선택(Ctrl/Shift 클릭) 로직 구현
- [x] 선택된 항목 수 및 파일 정보(해상도, 크기) 하단 상태바 표시
- [x] 업로드된 소스 목록 관리 서비스(`studio_state.js`) 초기화
## 5. 설계영역과의 분리 검증
- [x] `/studio` 페이지 로딩 시 설계영역의 X6 인스턴스가 생성되지 않아 리소스가 낭비되지 않는지 확인
- [x] 뒤로가기 또는 '설계영역으로 돌아가기' 버튼 정상 작동 확인
+19
View File
@@ -0,0 +1,19 @@
## 1. PNG to SVG 변환 엔진 (Transformation)
- [x] 브라우저 기반 벡터라이징 라이브러리(Potrace) 통합 완료
- [x] 소스 리스트의 PNG 파일을 읽어 실제 벡터 `<path>` 데이터로 추출하는 Processor 모듈 구현
- [x] 성능 최적화: 대량의 파일 변환 시 순차 처리 로직 구현
## 2. 변환 품질 검수 및 비교 UI (Review)
- [x] **Side-by-Side 뷰**: 원본 PNG와 변환된 SVG를 동일 크기로 나란히 배치하여 시각적 비교 지원
- [x] **가변 옵션 조절**: 기본 임계값 적용 및 벡터화 결과 즉시 반영 로직 구현
- [x] **최종 채택 토글**: 개별 에셋별로 [PNG 유지] 또는 [SVG 채택] 중 하나를 명시적으로 선택하는 기능
## 3. 다중 선택 및 벌크 속성 편집 (Bulk Edit)
- [x] 그리드 내 여러 에셋 선택 시 우측 패널에 '일괄 편집' 모드 활성화
- [x] **공통 속성 적용**: 선택된 모든 에셋에 대해 카테고리 일괄 변경 기능 구현
- [x] **명명 규칙(Naming Rule)**: 파일명을 기반으로 ID 및 라벨 자동 생성 로직 반영
## 4. 메타데이터 및 상태 관리
- [x] 가공 중인 에셋들의 실시간 세션 상태 유지 (`studio/state.js`)
- [x] 유효성 검사: 필수 값(Label) 실시간 업데이트 반영
- [x] 작업 중인 이미지의 프리뷰 및 선택 상태 시각화
+23
View File
@@ -0,0 +1,23 @@
## 1. 패키지 메타데이터 입력 UI
- [x] 상단 툴바에 패키지 ID(영문/숫자/언더바), 이름, 버전 입력 필드 추가
- [x] 패키지 전용 벤더(Vendor) 및 아이콘 지정 기능 구현
- [x] 필수 값 입력 여부 유효성 검사 로직 추가
## 2. 서버 측 저장 API (Backend)
- [x] `api_routes.py``/api/studio/save-pack` POST 엔드포인트 구현
- [x] 서버의 `static/assets/packs/[pack_id]/` 디렉토리를 자동 생성하고 이미지 파일 저장
- [x] 가공된 에셋 정보를 바탕으로 최종 `package.json` 파일 생성 및 저장
## 3. Build & Publish 워크플로우
- [x] `[Build Package]` 버튼 클릭 시 가공된 모든 에셋(SVG/PNG 선택본)을 수집하여 서버로 전송
- [x] 진행 상태 표시(Progress Bar) 및 성공/실패 알림 모달 구현
- [x] 성공 시 생성된 패키지 요약 정보(에셋 개수, 용량 등) 표시
## 4. 설계영역(Designer) 즉시 반영
- [x] 패키지 저장 후 설계영역으로 이동 시 신규 패키지가 라이브러리에 자동 로드되는지 확인
- [x] 라이브러리 내 카테고리가 `package.json`의 정의대로 올바르게 그룹화되는지 검증
- [x] 새로 추가된 에셋을 캔버스에 드랍했을 때 이미지 경로가 정상적으로 해석되는지 최종 확인
## 5. 데이터 모델 확장 및 검증 (Future Proof)
- [x] 향후 Rack 배치를 고려한 `Front/Back View` 정보가 `package.json`에 포함될 수 있는 구조인지 재확인
- [x] 패키지 삭제 또는 덮어쓰기(Overwrite) 시의 안정성 확보
+33
View File
@@ -0,0 +1,33 @@
# Object Studio 테스트 시나리오 (Verification)
본 시나리오는 스튜디오의 핵심 가공 기능(Phase 3)을 검증하기 위한 절차입니다.
## 시나리오: 멀티 벤더 패키지 제작 및 설계영역 연동
### 1. 이미지 준비 및 수집 (Ingestion)
* **파일 로드**: 로컬의 PNG 아이콘 파일 3~5개를 준비하여 워크스페이스로 드래그 앤 드롭함.
* **검증**: 좌측 소스 리스트에 파일명이 나타나고, 중앙 그리드에 실시간 프리뷰가 렌더링되는지 확인.
### 2. 단일 에셋 정밀 가공 (Processing)
* **변환 실행**: 그리드에서 하나의 PNG 파일을 선택하고 우측 패널의 `[Convert to SVG]` 버튼을 누름.
* **비교 검수**:
* 화면에 나타나는 [Original PNG]와 [Vectorized SVG]의 품질을 비교.
* 임계값(Threshold) 바를 조절하여 외곽선이 깔끔하게 추출되는지 확인.
* `[채택(SVG)]` 버튼을 눌러 결과가 반영되는지 확인.
* **속성 입력**: 에셋의 표시 이름을 'Core Router'로 변경하고 카테고리를 'Network'로 지정.
### 3. 벌크 속성 편집 (Bulk Edit)
* **다중 선택**: 나머지 3개의 이미지를 `Ctrl+Click`으로 선택.
* **일괄 적용**: 우측 패널에서 제조사(Vendor)를 'Cisco'로 일괄 입력하고 `[Apply]` 클릭.
* **검증**: 각 에셋을 개별 클릭했을 때 제조사 정보가 모두 'Cisco'로 동기화되었는지 확인.
### 4. 패키징 및 최종 발행 (Packaging)
* **정보 입력**: 상단 툴바의 패키지 명칭을 'My_Cisco_Pack'으로 입력.
* **발행**: `[Build Package]` 버튼을 눌러 서버에 에셋과 `package.json`이 저장되는지 확인.
* **연동 확인**: 설계영역(Draw)으로 돌아갔을 때 좌측 라이브러리에 'My_Cisco_Pack' 섹션이 새로 나타나는지 확인.
---
## 성공/실패 판정 기준
* **Success**: 모든 에셋이 `package.json`에 정의된 대로 라이브러리에 그룹화되어 나타남.
* **Failure**: 변환 중 브라우저 멈춤, 데이터 누락, 또는 설계영역에서 아이콘이 깨짐.
+118
View File
@@ -0,0 +1,118 @@
# DrawNET Object Studio 설계서 (Phase 1)
**Object Studio**는 단순한 이미지 업로드를 넘어, 인프라 설계에 필요한 전문적인 **에셋 관리 시스템(Asset Management System)**을 지향합니다.
## 1. 개요
* **목적**: 복잡한 인프라 소자(Cisco, Fortinet 등)를 패키지 단위로 관리하고, 전/후면 뷰 및 메타데이터를 정밀하게 가공하여 도면 제작 효율을 극대화함.
* **접근 주소**: `/studio` (독립형 페이지)
---
## 2. 주요 워크플로우
### [Ingestion: 소스 로드]
1. **벌크 로드**: 사용자가 로컬 폴더를 지정(`webkitdirectory`)하거나 여러 파일을 드래그 앤 드롭하여 소스 목록에 추가.
2. **프리뷰 그리드**: 업로드된 이미지들을 썸네일 형태로 시각화하여 확인.
### [Processing: 에셋 가공 및 속성 정의]
1. **개별/벌크 편집**:
* 선택된 오브젝트의 이름 변경, 속성 지정(아이콘 타입, 유형 등), 카테고리 매칭.
* 다중 선택 시 '제조사(Vendor)', '모델 시리즈', '환경' 등 공통 정보를 일괄 적용.
2. **PNG to SVG 하이브리드 변환**:
* PNG 소스를 벡터화(SVG Trace)하여 변환 결과 프리뷰 제공.
* 사용자가 **[원본 PNG 유지]** 또는 **[벡터 SVG 채택]** 중 선택. (SVG 채택 시 고해상도 무한 확대 및 색상 커스터마이징 가능)
### [Packaging: 내보내기 및 등록]
1. **패키지(Pack) 생성**: 가공 완료된 에셋들을 하나의 논리적 팩(예: `Cisco_Nexus_9000_Pack`)으로 묶음.
2. **뷰 매칭 (Front/Back)**: 랙 배치를 고려하여 동일 모델의 전면 아이콘과 후면 아이콘을 하나의 에셋 데이터로 바인딩.
3. **최종 발행**: 서버 저장소(`static/assets/packs/`)에 저장하고 메인 에디터의 에셋 라이브러리에 패키지 정보 업데이트.
---
## 3. 화면 UI/UX 설계 (안)
### 레이아웃 구조
* **좌측 (Source Panel)**: 업로드된 원본 파일 목록 및 Drag & Drop 영역.
* **중앙 (Workspace)**: 바둑판식(Grid) 에셋 프리뷰 영역 및 선택 도구.
* **우측 (Property Panel)**: 선택된 에셋(들)의 속성 입력창 (명칭, 제조사, 카테고리 등) 및 SVG 변환 비교 뷰.
### 주요 기능 버튼
* `[Load Folder]`: 폴더 단위 소스 로드.
* `[Batch Edit]`: 선택 항목 일괄 속성 적용.
* `[Vectorize]`: 벡터 변환 및 품질 검수 모드 진입.
* `[Build Pack]`: 최종 패키지 추출 및 서버 등록.
---
## 4. 데이터 모델 확장 (Draft)
```json
{
"pack_id": "cisco_sw_pack",
"vendor": "Cisco",
"version": "1.0.0",
"assets": [
{
"id": "c9300L_24T",
"label": "Catalyst 9300L 24T",
"category": "Switch",
"views": {
"front": "packs/cisco/9300l_f.svg",
"back": "packs/cisco/9300l_b.svg"
},
"specs": {
"u_height": 1,
"depth": "350mm"
}
}
]
}
```
---
## 6. 구현 단계별 로드맵 (Roadmap)
안정적인 연동과 품질 관리를 위해 다음 순서로 개발을 진행합니다.
### Phase 1: 설계영역 수용성 및 인프라 구축 (Infrastructure)
* **목표**: 스튜디오에서 만든 패키지를 설계영역에서 즉시 읽어 들일 수 있는 기반 마련.
* **주요 작업**:
* `package.json` 데이터 규격 확정.
* 설계영역 에셋 라이브러리의 '멀티 패키지 로더' 구현.
* 샘플 패키지(`Cisco_Sample`)를 통한 연동 테스트.
* **체크리스트**: `/docs/studio/phase1_checklist.md`
### Phase 2: 스튜디오 기반 구축 (Base Setup)
* **목표**: 스튜디오 독립 페이지 및 이미지 Ingestion 엔진 구현.
* **주요 작업**:
* `/studio` 라운팅 및 전용 HTML/CSS 레이아웃 생성.
* 폴더 선택 및 드래그 앤 드롭 업로드 구현.
* 에셋 프리뷰 그리드 UI 최적화.
* **체크리스트**: `/docs/studio/phase2_checklist.md`
### Phase 3: 스튜디오 핵심 가공 기능 (Core Processing)
* **목표**: PNG to SVG 변환 및 벌크 속성 편집 기능 구현.
* **주요 작업**:
* 벡터라이징(Trace) 엔진 탑재 및 변환 전/후 비교 UI.
* 다중 선택 기반 일괄 속성(Vendor, Category 등) 편집기.
* **체크리스트**: `/docs/studio/phase3_checklist.md`
* **체크리스트**: `/docs/studio/phase4_checklist.md` [v] **Completed**
---
## 7. 최종 구현 사양 (Final Implementation)
### 아키텍처 (Architecture)
- **프론트엔드 모듈화**: 코드 복잡도를 제어하기 위해 관심사별로 모듈을 분리함.
- `index.js`: 엔트리 포인트 및 이벤트 바인딩.
- `renderer.js`: 모든 DOM 생성 및 UI 업데이트 담당.
- `actions.js`: 빌드, 변환, 벌크 수정 등 비즈니스 로직.
- `state.js` / `ingester.js` / `processor.js`: 상태 관리 및 데이터 처리 전담.
- **백엔드**: Flask (`api_routes.py`) 기반의 멀티 패키지 스캔 및 저장 엔진 구축.
### 주요 기술 스택
- **Potrace-js**: 클라이언트 사이드 고속 벡터화.
- **Flask FormData**: 대용량 이미지 및 메타데이터 일괄 전송.
- **X6 Data Integration**: `package.json` 규격을 통한 설계 영역과의 심리스한 연동.
+43
View File
@@ -0,0 +1,43 @@
# drawNET Development Task List (Updated 2026-03-19)
## 📋 현재 상태 요약
- **완료:** AntV X6 엔진 마이그레이션, DSL 동기화 기초, 정밀 배치 도구, UX 고도화.
- **핵심 성과:** **Object Studio(Phase 1-4) 구현 완료**. 이제 독립된 환경에서 자산을 제작하고 패키징하여 도면에 즉시 반영 가능.
## ✅ 완료된 작업 (Object Studio Milestone)
### Phase 1: 자산 인프라 및 멀티 로더
- [x] `package.json` 데이터 규격 정의 (`vendor`, `pack_id`, `views` 등)
- [x] 백엔드 멀티 패키지 스캔 엔진 (`/assets`) 고도화
- [x] 설계영역(Designer) 사이드바의 벤더별 패키지 자동 로딩 구현
### Phase 2: 스튜디오 기반 구축
- [x] 독립된 제작 페이지 (`/studio`) 라우팅 및 전용 레이아웃 (3-Panel)
- [x] 폴더/파일 단위 대량 이미지 인제스천(Ingestion) 기능
- [x] 작업 영역(Grid) 및 실시간 필터링 UI
### Phase 3: 핵심 가공 및 벡터 변환
- [x] Potrace-js 통합을 통한 PNG -> SVG 실시간 벡터라이징
- [x] **Side-by-Side 리뷰 UI**: 원본과 변환본 비교 및 최종 포맷 채택 기능
- [x] 벌크(Bulk) 속성 편집(Category 일괄 적용 등) 기능
### Phase 4: 패키징 및 최종 연동
- [x] **/api/studio/save-pack**: 가공된 에셋 및 `package.json` 서버 저장 API
- [x] 버튼 하나로 빌드부터 설계영역 반영까지 워크플로우 자동화
- [x] **코드 모듈화(Refactoring)**: `Renderer`, `Actions` 모듈 분리로 코드 품질 확보
- [x] **PPTX 내보내기 (Simple Test)**: PptxGenJS 기반 도면 슬라이드 추출 기능 구현 (설계영역)
---
## 🚀 향후 개발 우선순위
### 1단계: 엔터프라이즈 기능 확장
- [ ] **Sub-Graph (Nesting)**: 그룹 내부 진입 및 상세 설계 드릴다운 기능
- [ ] **Rack View 가공**: 스튜디오에서 장비의 U-Height 정보를 기반으로 한 랙 실장 뷰 연동
### 2단계: 분석 및 자동화
- [ ] **Live Status Monitoring**: 실시간 Ping 체크를 통한 장애 가시화
- [ ] **Compliance Audit**: 설계 보안 가이드 준수 여부 자동 분석
### 3단계: AI 어시스턴트
- [ ] LLM 연동을 통한 자연어 기반 토폴로지 자동 생성 및 최적화 제안
+39
View File
@@ -0,0 +1,39 @@
import os
import re
import requests
css_path = r'c:\project\drawNET\static\css\lib\google-fonts.css'
fonts_dir = r'c:\project\drawNET\static\fonts'
if not os.path.exists(fonts_dir):
os.makedirs(fonts_dir)
with open(css_path, 'r', encoding='utf-8') as f:
content = f.read()
urls = re.findall(r'url\((https://fonts\.gstatic\.com/s/[^)]+)\)', content)
urls = list(set(urls)) # Unique URLs
url_map = {}
for i, url in enumerate(urls):
filename = url.split('/')[-1]
local_path = os.path.join(fonts_dir, filename)
print(f"Downloading {url} -> {filename}")
try:
r = requests.get(url)
if r.status_code == 200:
with open(local_path, 'wb') as f:
f.write(r.content)
url_map[url] = f"/static/fonts/{filename}"
except Exception as e:
print(f"Failed to download {url}: {e}")
# Update CSS
new_content = content
for url, local_url in url_map.items():
new_content = new_content.replace(url, local_url)
with open(css_path, 'w', encoding='utf-8') as f:
f.write(new_content)
print("Google Fonts localized successfully.")
View File
+58
View File
@@ -0,0 +1,58 @@
# drawNET Premium 실무 가이드 (Advanced Engineering)
본 가이드는 drawNET Premium 에디션을 사용하여 복잡한 네트워크 아키텍처와 시스템 토폴로지를 설계하는 엔지니어들을 위한 핵심 지침서입니다.
---
## 1. 핵심 철학: "Drawing for Context, Data for Mapping"
drawNET은 단순한 다이어그램 도구가 아닙니다. **"시각적 아키텍처(Drawing)"**와 **"엔지니어링 데이터(Inventory)"**를 하나로 통합합니다.
- 모든 디자인 요소는 배후에 데이터 속성(IP, 모델, 상태 등)을 가지고 있으며, 이는 자동으로 보고서와 연동됩니다.
---
## 2. 캔버스 네비게이션
- **스마트 팬닝**: 마우스 **우측 버튼** 드래그 또는 `Space` + 드래그로 캔버스를 이동합니다.
- **정밀 줌**: `Ctrl` + 마우스 휠을 사용하여 **커서가 가리키는 지점**을 중심으로 확대/축소합니다.
- **화면 맞춤**: `Ctrl + F`를 눌러 모든 요소를 한눈에 보이게 정렬합니다.
---
## 3. 지능형 자산(Asset) 활용
- **패키지 관리**: 왼쪽 라이브러리 상단의 **필터 아이콘**을 클릭하여 필요한 제조사/분류 패키지만 활성화할 수 있습니다.
- **카테고리 토글**: 카테고리 헤더를 클릭하여 사용하지 않는 자산 목록을 접어 가독성을 높이세요.
- **Fixed Objects**: 기본 도형(`Rect`, `Circle`, `Text` 등)은 상단 고정 섹션에서 언제든 꺼내 쓸 수 있습니다.
---
## 4. 고급 조작 및 편집 (Gestures)
- **즉시 복제 (Cloning)**: 노드를 선택한 후 **`Ctrl` + 드래그**하면 고스트 효과와 함께 즉시 복제본이 생성됩니다.
- **중첩 선택 (Recursive Select)**: 복잡하게 겹쳐진 구간에서 **`Ctrl` + 우클릭**을 하면 해당 지점의 모든 객체 목록이 나타나 원하는 레이어를 정확히 선택할 수 있습니다.
- **포맷 페인터 (Style Painter)**: 한 객체의 스타일을 다른 객체에 동일하게 적용하려면 `Ctrl + Shift + C`(복사) -> `Ctrl + Shift + V`(붙여넣기)를 사용하세요.
---
## 5. 레이어 시스템 (Standard vs Logical)
drawNET Premium은 목적에 따른 두 가지 레이어 유형을 제공합니다.
- **Standard Layer (물리)**: 실제 장비 배치 및 물리적 연결을 설계합니다.
- **Logical Layer (논리)**: 보안 구역, 데이터 흐름, 서비스 연관도 등 추상적인 개념을 설계합니다. 이 레이어에서는 복제 시 데이터의 논리적 무결성이 보호됩니다.
---
## 6. 스마트 라우팅 (Connectivity)
- **Manhattan (`Shift + A`)**: 장애물을 회피하는 직각 경로로 자동 연결합니다.
- **Straight (`Shift + S`)**: 최단 거리 직선으로 연결합니다.
- **선 스타일 전환**: 이미 그려진 선을 선택하고 `Alt + M`(Manhattan) 또는 `Alt + L`(Straight)로 즉시 변경할 수 있습니다.
- **연결 해제**: 두 노드를 선택하고 `Shift + D`를 누르면 사이의 모든 연결이 제거됩니다.
---
## 7. 전문 보고서 및 인벤토리 추출
설계가 완료되면 상단 메뉴를 통해 다음 형식으로 성과물을 내보낼 수 있습니다.
- **PPTX 보고서**: 각 노드별 상세 설명과 인벤토리 통계가 포함된 고품질 발표 자료를 생성합니다.
- **Excel 인벤토리**: 모든 자산의 상세 속성(IP, 모델, 태그 등)을 집계하여 관리 대장으로 활용합니다.
- **Vectored Export**: PNG/SVG/PDF를 통해 깨짐 없는 고해상도 아키텍처 이미지를 확보합니다.
---
*최종 업데이트: 2026-03-22*
*drawNET Engineering Team*
+50
View File
@@ -0,0 +1,50 @@
# 리치 텍스트 카드(Rich Text Card) 활용 가이드 🃏
'리치 텍스트 카드'는 아키텍처 도면의 **설명(구축 내역)**이나 **범례(Legend)**를 전문적으로 표현하기 위해 설계된 스마트 객체입니다.
---
## 1. 기본 사용법
1. **에셋 선택**: 왼쪽 '고정 오브젝트(Fixed Objects)' 목록 최하단의 **'리치 텍스트 카드'**를 캔버스로 드래그합니다.
2. **속성 편집**: 오브젝트를 선택하면 오른쪽 사이드바에 편집 메뉴가 나타납니다.
3. **내용 입력**: '내용' 칸에 텍스트를 입력하면 실시간으로 카드 본문에 반영됩니다. (줄바꿈 지원)
---
## 2. 카드 유형별 특징
### ❶ 번호 매기기 (Numbered List)
- **용도**: 구축 내역, 작업 순서, 시스템 특징 나열 등.
- **특징**: 각 줄마다 헤더 색상과 동일한 **파란색 번호 박스**가 자동으로 생성됩니다.
- **팁**: 단순히 엔터(`\n`)로 줄을 나누기만 하면 번호가 순차적으로 부여됩니다.
### ❷ 불렛 기호 (Bullet Points)
- **용도**: 일반적인 특징 설명, 비고 사항 등.
- **특징**: 깔끔한 원형(`●`) 불렛 기호가 각 줄 앞에 붙습니다.
### ❸ 범례 모드 (Legend Mode) ⭐
- **용도**: 도면 하단의 기호 설명(범례) 작성.
- **특징**: 텍스트 기반의 특수 문법을 사용하여 **아이콘을 포함한 범례**를 자동으로 렌더링합니다.
#### 💡 범례 모드 문법 (Syntax):
본문에 아래와 같이 입력하세요:
- `- (circle:#색상코드) 설명` : 지정된 색상의 **원형** 아이콘 생성
- `- (rect:#색상코드) 설명` : 지정된 색상의 **사각형** 아이콘 생성
- `- (line:#색상코드) 설명` : 지정된 색상의 **가로 선** 아이콘 생성
> **입력 예시:**
> `- (circle:#10b981) 가동 중인 시스템`
> `- (rect:#ef4444) 장애 발생 구역`
> `- (line:#f97316) 외부 연동 구간`
---
## 3. 스타일 커스터마이징
- **헤더 제목**: 도면의 맥락에 맞게 '구축 내역', '범례' 등으로 수정하세요.
- **테마 색상**: 헤더의 색상 피커를 사용하여 프로젝트 테마에 맞게 색상을 변경할 수 있습니다. (테두리 색상도 자동으로 동기화됩니다.)
- **잠금 기능**: 배치가 완료된 후 'Locked'를 체크하면 의도치 않은 이동이나 삭제를 방지할 수 있습니다.
---
*최종 수정일: 2026-03-21*
*drawNET Design System*
+69
View File
@@ -0,0 +1,69 @@
# drawNET Keyboard Shortcuts (Premium Edition)
이 문서는 drawNET의 실제 시스템 설정(`hotkeys.json`)과 일치하는 최신 단축키 안내를 포함합니다.
## 1. 기본 조작 및 뷰 (Navigation & View)
| 단축키 | 기능 | 설명 |
| :--- | :--- | :--- |
| `Ctrl` + `F` | 화면 맞춤 (Fit) | 모든 노드가 화면에 들어오도록 줌 및 정렬 조정 |
| `Ctrl` + `+` / `=` | 확대 (Zoom In) | 캔버스 확대 |
| `Ctrl` + `-` | 축소 (Zoom Out) | 캔버스 축소 |
| `Ctrl` + `Mouse Wheel` | 정밀 줌 | **마우스 커서 위치**를 중심으로 확대/축소 |
| `Alt` + `0` | 인벤토리 패널 | 자산 수량 집계 패널 토글 |
| `Alt` + `1` **또는** `Alt` + `Enter` | 속성 사이드바 | 선택된 오브젝트의 상세 속성 편집창 토글 |
| `Alt` + `9` | 레이어 패널 | 논리적 계층 관리 및 가시성 제어 패널 토글 |
| `Space` + `Drag` | 캔버스 팬닝 | 캔버스를 상하좌우로 자유롭게 이동 |
| **`Right Click`** + `Drag` | **스마트 팬닝** | 마우스 우측 버튼 드래그로 즉시 팬닝 (문맥 메뉴와 구분됨) |
| **`Ctrl` + `Right Click`** | **재귀적 오브젝트 선택** | 겹친 지점의 모든 오브젝트 목록을 보여줌 (Photoshop 스타일) |
## 2. 편집 및 수정 (Editing)
| 단축키 | 기능 | 설명 |
| :--- | :--- | :--- |
| `Delete` / `Backspace` | 삭제 | 선택된 노드 또는 선을 삭제 |
| `Esc` | 모든 선택 해제 | 선택된 모든 요소를 해제하고 속성 창을 닫음 |
| `Ctrl` + `Z` | 실행 취소 (Undo) | 마지막 작업 취소 |
| `Ctrl` + `Y` / `Ctrl` + `Shift` + `Z` | 다시 실행 (Redo) | 취소한 작업 다시 실행 |
| `Ctrl` + `C` | 복사 (Copy) | 선택된 오브젝트들을 클립보드에 복사 |
| `Ctrl` + `V` | 붙여넣기 (Paste) | 복사된 오브젝트들을 마우스 위치에 붙여넣기 |
| **`Ctrl` + `Drag`** | **즉시 복제 (Cloning)** | 선택된 노드를 드래그하여 즉시 복제 (고스트 효과 동반) |
| `Ctrl` + `D` | 제자리 복제 (Duplicate) | 선택된 오브젝트를 제자리에 즉시 복제 |
| `Ctrl` + `G` | 그룹화 (Group) | 선택된 노드들을 하나의 그룹으로 묶음 |
| `F2` | 이름 변경 | 선택된 오브젝트의 **속성창 라벨 입력칸으로 포커스 이동** |
| `Shift` + `A` | 자동 연결 (Manhattan) | 선택된 노드 사이를 장애물 회피 경로로 연결 |
| `Shift` + `S` | 직선 연결 (Straight) | 선택된 노드 사이를 직선으로 연결 |
| `Alt` + `M` | 선 스타일 전환 | 선택된 선을 Manhattan(자동) 스타일로 변경 |
| `Alt` + `L` | 선 스타일 전환 | 선택된 선을 Straight(직선) 스타일로 변경 |
| `Shift` + `D` | 연결 끊기 | 선택된 노드들 사이의 모든 연결 제거 |
| `[` | 뒤로 보내기 | 선택된 오브젝트를 시각적으로 한 단계 뒤로 보냄 |
| `]` | 앞으로 가져오기 | 선택된 오브젝트를 시각적으로 한 단계 앞으로 가져옴 |
| `Ctrl` + `L` | 오브젝트 잠금 | 선택된 오브젝트의 이동 및 삭제 방지 (시각적 점선 표시) |
| **`Ctrl` + `Shift` + `L`** | 자동 레이아웃 | 노드들을 계층 구조에 따라 자동으로 재배치 |
| **`Ctrl` + `Shift` + `C`** | **포맷 복사** | 선택된 노드/엣지의 스타일(색상, 크기 등)을 복사 |
| **`Ctrl` + `Shift` + `V`** | **포맷 붙여넣기** | 복사한 스타일을 선택한 대상에 즉시 적용 |
## 3. 정렬 및 배치 (Alignment & Distribution)
| 단축키 | 기능 | 설명 |
| :--- | :--- | :--- |
| `Shift` + `1` ~ `4` | 정렬 (Align) | 상단, 하단, 왼쪽, 오른쪽 정렬 |
| `Shift` + `5` ~ `6` | 중앙 정렬 (Center) | 수직 중앙(Middle), 수평 중앙(Center) 정렬 |
| `Shift` + `7` ~ `8` | 균등 배치 (Distribute) | 가로 간격 동일하게, 세로 간격 동일하게 배치 |
## 4. 노드 이동 (Movement)
| 단축키 | 기능 | 설명 |
| :--- | :--- | :--- |
| `Arrow Keys` | 미세 이동 | 선택된 노드를 5px 단위로 정밀 이동 |
| `Shift` + `Arrow Keys` | 격자 이동 | 선택된 노드를 그리드 설정 간격으로 이동 |
## 5. 파일 관리 (File Management)
| 단축키 | 기능 | 설명 |
| :--- | :--- | :--- |
| `Ctrl` + `S` | 저장 (Export) | 현재 상태를 `.dnet` JSON 파일로 내보내기 |
| `Ctrl` + `O` | 불러오기 (Import) | `.dnet` 파일 불러오기 |
---
+4
View File
@@ -0,0 +1,4 @@
Flask>=3.0.0
requests>=2.31.0
gunicorn>=21.2.0
python-dotenv>=1.0.0
View File
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
import os
from flask import Blueprint, render_template, send_from_directory
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
return render_template('index.html')
@main_bp.route('/studio')
def studio():
return render_template('studio.html')
@main_bp.route('/manual/<path:filename>')
def serve_manual(filename):
return send_from_directory(os.path.join(os.getcwd(), 'manual'), filename)
+70
View File
@@ -0,0 +1,70 @@
{
"categories": [
{
"id": "Network",
"label": "Network Devices",
"icon": "router.svg"
},
{
"id": "Security",
"label": "Security & Firewalls",
"icon": "firewall.svg"
}
],
"assets": [
{
"id": "router",
"type": "Router",
"category": "Network",
"label": "Standard Router",
"path": "router.svg",
"format": "svg",
"vendor": "Cisco"
},
{
"id": "switch",
"type": "Switch",
"category": "Network",
"label": "L3 Switch",
"path": "switch.svg",
"format": "svg",
"vendor": "Cisco"
},
{
"id": "firewall_cisco",
"type": "Firewall",
"category": "Security",
"label": "Cisco Firewall",
"path": "firewall.svg",
"format": "svg",
"vendor": "Cisco"
},
{
"id": "firewall_png",
"type": "F/W",
"category": "Security",
"label": "Legacy F/w",
"path": "firewall.png",
"format": "png",
"vendor": "Generic"
},
{
"id": "server",
"type": "Server",
"category": "Compute",
"label": "Enterprise Server",
"path": "server.svg",
"format": "svg",
"vendor": "Generic"
},
{
"id": "cloud",
"type": "Internet",
"category": "External",
"label": "External Cloud",
"path": "cloud.svg",
"format": "svg",
"vendor": "Generic"
}
]
}
+3
View File
@@ -0,0 +1,3 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path d="M25 40 A15 15 0 0 1 50 25 A25 25 0 0 1 85 50 A20 20 0 0 1 70 85 H30 A20 20 0 0 1 25 40" fill="#cbd5e1" stroke="#64748b" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect x="15" y="20" width="70" height="60" rx="4" fill="#ef4444" stroke="#b91c1c" stroke-width="2"/>
<path d="M15 40 H85 M15 60 H85 M35 20 V40 M65 20 V40 M25 40 V60 M55 40 V60 M85 40 V60 M40 60 V80 M70 60 V80" stroke="white" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -0,0 +1,125 @@
{
"id": "cisco_base_pack",
"name": "cisco_pack",
"vendor": "Unknown",
"version": "1.0.0",
"assets": [
{
"id": "cisco_firewall",
"label": "Cisco Firewall",
"category": "Security",
"views": {
"icon": "cisco_firewall.png",
"front": "cisco_firewall.png"
}
},
{
"id": "cisco_firewallservicemodule",
"label": "Cisco Firewall service module",
"category": "Security",
"views": {
"icon": "cisco_firewallservicemodule.png",
"front": "cisco_firewallservicemodule.png"
}
},
{
"id": "cisco_generic",
"label": "Cisco Generic",
"category": "Other",
"views": {
"icon": "cisco_generic.png",
"front": "cisco_generic.png"
}
},
{
"id": "cisco_iosslb",
"label": "Cisco Iosslb",
"category": "Network",
"views": {
"icon": "cisco_iosslb.png",
"front": "cisco_iosslb.png"
}
},
{
"id": "cisco_laptop",
"label": "Cisco Laptop",
"category": "Device",
"views": {
"icon": "cisco_laptop.png",
"front": "cisco_laptop.png"
}
},
{
"id": "cisco_printer",
"label": "Cisco Printer",
"category": "Device",
"views": {
"icon": "cisco_printer.png",
"front": "cisco_printer.png"
}
},
{
"id": "cisco_wirelessrouter",
"label": "Cisco Wirelessrouter",
"category": "Network",
"views": {
"icon": "cisco_wirelessrouter.png",
"front": "cisco_wirelessrouter.png"
}
},
{
"id": "cisco_wwwserver",
"label": "Cisco Wwwserver",
"category": "Device",
"views": {
"icon": "cisco_wwwserver.png",
"front": "cisco_wwwserver.png"
}
},
{
"id": "cisco_contentservicerouter",
"label": "Cisco Contentservicerouter",
"category": "Network",
"views": {
"icon": "cisco_contentservicerouter.png",
"front": "cisco_contentservicerouter.png"
}
},
{
"id": "cisco_database",
"label": "Cisco Database",
"category": "DBMS",
"views": {
"icon": "cisco_database.png",
"front": "cisco_database.png"
}
},
{
"id": "cisco_mgx8000",
"label": "Cisco Mgx8000",
"category": "Network",
"views": {
"icon": "cisco_mgx8000.png",
"front": "cisco_mgx8000.png"
}
},
{
"id": "cisco_l2_100base",
"label": "Cisco L2 ",
"category": "Network",
"views": {
"icon": "cisco_l2_100base.png",
"front": "cisco_l2_100base.png"
}
},
{
"id": "cisco_cloud",
"label": "Cisco Cloud",
"category": "Cloud",
"views": {
"icon": "cisco_cloud.png",
"front": "cisco_cloud.png"
}
}
]
}
@@ -0,0 +1,24 @@
{
"id": "sample_cisco",
"name": "Cisco Enterprise Pack",
"vendor": "Cisco",
"version": "1.0.0",
"assets": [
{
"id": "cisco_router",
"label": "Integrated Services Router",
"category": "Network",
"views": {
"icon": "router.svg"
}
},
{
"id": "cisco_switch",
"label": "Catalyst Switch",
"category": "Network",
"views": {
"icon": "switch.svg"
}
}
]
}
@@ -0,0 +1,5 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" fill="#3b82f6" stroke="#1d4ed8" stroke-width="2"/>
<path d="M50 20 L50 40 M50 80 L50 60 M20 50 L40 50 M80 50 L60 50" stroke="white" stroke-width="5" stroke-linecap="round"/>
<path d="M45 40 L50 35 L55 40 M45 60 L50 65 L55 60 M40 45 L35 50 L40 55 M60 45 L65 50 L60 55" fill="none" stroke="white" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

@@ -0,0 +1,5 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="25" width="80" height="50" rx="5" fill="#3b82f6" stroke="#1d4ed8" stroke-width="2"/>
<path d="M30 40 L50 40 M50 60 L70 60" stroke="white" stroke-width="4" stroke-linecap="round"/>
<path d="M45 35 L50 40 L45 45 M55 55 L50 60 L55 65" fill="none" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" fill="#3b82f6" stroke="#1d4ed8" stroke-width="2"/>
<path d="M50 20 L50 40 M50 80 L50 60 M20 50 L40 50 M80 50 L60 50" stroke="white" stroke-width="5" stroke-linecap="round"/>
<path d="M45 40 L50 35 L55 40 M45 60 L50 65 L55 60 M40 45 L35 50 L40 55 M60 45 L65 50 L60 55" fill="none" stroke="white" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

+7
View File
@@ -0,0 +1,7 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect x="25" y="10" width="50" height="80" rx="2" fill="#64748b" stroke="#334155" stroke-width="2"/>
<rect x="30" y="20" width="40" height="5" fill="#94a3b8"/>
<rect x="30" y="35" width="40" height="5" fill="#94a3b8"/>
<rect x="30" y="50" width="40" height="5" fill="#94a3b8"/>
<circle cx="35" cy="75" r="3" fill="#22c55e"/>
</svg>

After

Width:  |  Height:  |  Size: 440 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="25" width="80" height="50" rx="5" fill="#3b82f6" stroke="#1d4ed8" stroke-width="2"/>
<path d="M30 40 L50 40 M50 60 L70 60" stroke="white" stroke-width="4" stroke-linecap="round"/>
<path d="M45 35 L50 40 L45 45 M55 55 L50 60 L55 65" fill="none" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

File diff suppressed because one or more lines are too long
+306
View File
@@ -0,0 +1,306 @@
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(/static/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxTN1OVgaY.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxTPlOVgaY.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxTOVOVgaY.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxTNVOVgaY.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxTNFOVgaY.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxTOlOV.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+120
View File
@@ -0,0 +1,120 @@
/* analysis.css - Theme-aware styles for Inventory and Analysis Panels */
.inventory-panel {
position: absolute;
bottom: 24px;
right: 24px;
width: 260px;
max-height: 320px;
z-index: 1000;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: auto !important;
}
.inventory-header {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
font-weight: 800;
color: var(--text-color);
border-bottom: 1px solid var(--panel-border);
padding-bottom: 10px;
letter-spacing: -0.02em;
}
.inventory-header i {
color: var(--accent-color);
font-size: 14px;
}
.inventory-list {
display: flex;
flex-direction: column;
gap: 6px;
overflow-y: auto;
padding-right: 4px;
}
/* Custom Scrollbar for Inventory */
.inventory-list::-webkit-scrollbar {
width: 4px;
}
.inventory-list::-webkit-scrollbar-track {
background: transparent;
}
.inventory-list::-webkit-scrollbar-thumb {
background: var(--panel-border);
border-radius: 4px;
}
.inventory-item {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--item-bg);
padding: 8px 14px 8px 18px;
border-radius: 12px;
border: 1px solid var(--panel-border);
transition: all 0.2s ease;
}
.inventory-item:hover {
background: var(--item-hover-bg);
transform: translateX(-2px);
}
.inventory-item .lbl {
font-weight: 600;
color: var(--sub-text);
font-size: 11px;
text-transform: capitalize;
}
.inventory-item .val {
font-weight: 800;
color: var(--accent-color);
background: rgba(59, 130, 246, 0.1);
padding: 2px 8px;
border-radius: 6px;
font-size: 12px;
min-width: 24px;
text-align: center;
}
.inventory-footer {
margin-top: auto;
padding-top: 12px;
border-top: 1px solid var(--panel-border);
}
.btn-export-bom {
width: 100%;
padding: 10px;
background: var(--accent-color);
color: white;
border: none;
border-radius: 12px;
font-size: 11px;
font-weight: 800;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-export-bom:hover {
background: #2563eb;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
}
.btn-export-bom i {
font-size: 12px;
}
+32
View File
@@ -0,0 +1,32 @@
/* Animations */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-up {
animation: fadeInUp 0.6s cubic-bezier(0.23, 1, 0.32, 1) forwards;
}
/* Edge Flow Animation */
@keyframes drawnet-flow {
from { stroke-dashoffset: 20; }
to { stroke-dashoffset: 0; }
}
.flow-animation path {
stroke-dasharray: 5, 5;
animation: drawnet-flow 1s linear infinite;
}
/* Modal Animations */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes scaleUp { from { opacity: 0; transform: scale(0.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } }
@keyframes scaleDown { from { opacity: 1; transform: scale(1) translateY(0); } to { opacity: 0; transform: scale(0.9) translateY(20px); } }
.animate-fade-in { animation: fadeIn 0.3s ease forwards; }
.animate-fade-out { animation: fadeOut 0.2s ease forwards; }
.animate-scale-up { animation: scaleUp 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards; }
.animate-scale-down { animation: scaleDown 0.3s ease forwards; }
+175
View File
@@ -0,0 +1,175 @@
.asset-library {
padding: 0 16px;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.asset-library h3 {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--sub-text);
margin: 24px 16px 12px;
white-space: nowrap;
font-weight: 800;
}
.sidebar.collapsed .asset-library h3 {
opacity: 0;
}
.asset-category {
margin-bottom: 8px;
}
.category-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--item-bg);
border-radius: 12px;
cursor: pointer;
transition: background 0.2s;
}
.category-header:hover {
background: var(--item-hover-bg);
}
.category-header span {
font-size: 11px;
font-weight: 800;
color: var(--text-color);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.category-header i {
font-size: 10px;
color: var(--sub-text);
transition: transform 0.3s;
}
.asset-category.active .category-header i {
transform: rotate(180deg);
}
.category-content {
display: none;
padding: 12px 0;
}
.asset-category.active .category-content {
display: block;
}
.sidebar.collapsed .category-content {
display: none !important; /* Always hide content in sidebar when collapsed */
}
.asset-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
padding: 0 8px;
}
.asset-item {
background: var(--item-bg);
border: 1px solid var(--panel-border);
padding: 12px;
border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: grab;
transition: all 0.3s;
min-width: 0;
}
.asset-item span {
font-size: 9px;
color: var(--text-color);
text-align: center;
word-break: break-all;
line-height: 1.3;
opacity: 0.8;
}
.sidebar.collapsed .asset-item span {
display: none;
}
.asset-item:hover {
background: var(--item-hover-bg);
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.05);
}
.asset-item img {
pointer-events: none;
}
/* Fixed Category Optimization */
.fixed-category .asset-grid {
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.fixed-category .asset-item {
padding: 8px;
height: 44px;
width: 44px;
justify-content: center;
}
.fixed-category .asset-item span {
display: none;
}
.fixed-category .asset-item i,
.fixed-category .asset-item img {
margin-bottom: 0 !important;
}
.category-divider {
height: 1px;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.1), transparent);
margin: 20px 0;
position: relative;
}
.category-divider::after {
content: '-';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--sidebar-bg);
padding: 0 10px;
font-size: 10px;
color: var(--sub-text);
font-weight: 800;
}
.asset-group-label {
font-size: 9px;
font-weight: 800;
color: var(--sub-text);
padding: 12px 16px 6px;
text-transform: uppercase;
letter-spacing: 0.8px;
opacity: 0.7;
display: flex;
align-items: center;
gap: 8px;
}
.asset-group-label::after {
content: '';
flex: 1;
height: 1px;
background: rgba(255,255,255,0.05);
}
+140
View File
@@ -0,0 +1,140 @@
:root {
--bg-color: #f8fafc;
--bg-rgb: 248, 250, 252;
--text-color: #0f172a;
--accent-color: #3b82f6;
--panel-bg: rgba(255, 255, 255, 0.7);
--panel-border: rgba(255, 255, 255, 0.5);
--shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
--sub-text: #64748b;
--canvas-bg: #ffffff;
--grid-color: rgba(0, 0, 0, 0.05);
--grid-size: 20px;
--item-bg: rgba(255, 255, 255, 0.4);
--item-hover-bg: #e2e8f0;
}
[data-theme="dark"] {
--bg-color: #020617;
--bg-rgb: 2, 6, 23;
--text-color: #f8fafc;
--accent-color: #3b82f6;
--panel-bg: rgba(15, 23, 42, 0.8);
--panel-border: rgba(255, 255, 255, 0.1);
--shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
--sub-text: #cbd5e1;
--canvas-bg: #1e293b;
--grid-color: rgba(255, 255, 255, 0.1);
--grid-size: 20px;
--item-bg: rgba(255, 255, 255, 0.05);
--item-hover-bg: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
height: 100vh;
overflow: hidden;
}
.glass-panel {
background: var(--panel-bg);
backdrop-filter: blur(12px) saturate(180%);
border: 1px solid var(--panel-border);
box-shadow: var(--shadow);
border-radius: 32px;
}
/* Markdown Rendering Styles */
.markdown-body {
font-size: 14px;
line-height: 1.7;
color: var(--text-color);
}
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 800;
}
.markdown-body h1 { font-size: 1.8em; }
.markdown-body h2 {
font-size: 1.4em;
border-bottom: 1px solid var(--panel-border);
padding-bottom: 8px;
color: var(--accent-color);
}
.markdown-body h3 { font-size: 1.2em; }
.markdown-body p { margin-bottom: 16px; color: var(--sub-text); }
.markdown-body ul, .markdown-body ol {
padding-left: 20px;
margin-bottom: 16px;
}
.markdown-body li { margin-bottom: 8px; }
.markdown-body blockquote {
padding: 12px 20px;
margin: 16px 0;
background: var(--item-bg);
border-left: 4px solid var(--accent-color);
border-radius: 4px;
font-style: italic;
color: var(--sub-text);
}
.markdown-body code {
background: var(--item-bg);
padding: 2px 6px;
border-radius: 4px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.9em;
color: var(--accent-color);
}
.markdown-body pre {
background: var(--item-bg);
padding: 16px;
border-radius: 12px;
overflow-x: auto;
margin-bottom: 16px;
}
.markdown-body table {
width: 100%;
border-collapse: collapse;
margin-bottom: 24px;
border: 1px solid var(--panel-border);
border-radius: 12px;
overflow: hidden;
}
.markdown-body th, .markdown-body td {
padding: 12px 16px;
border-bottom: 1px solid var(--panel-border);
text-align: left;
}
.markdown-body th {
background: var(--item-bg);
font-weight: 800;
color: var(--accent-color);
}
.markdown-body img {
max-width: 100%;
border-radius: 16px;
border: 1px solid var(--panel-border);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
margin: 16px 0;
}
+49
View File
@@ -0,0 +1,49 @@
.editor-panel {
position: absolute;
bottom: 24px;
right: 24px;
width: 480px;
min-width: 400px;
height: 200px;
z-index: 90;
overflow: hidden;
display: flex;
flex-direction: column;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: bottom right;
}
.editor-panel.collapsed {
height: 40px; /* Header only height */
opacity: 0.8;
}
.editor-header {
padding: 12px 25px;
border-bottom: 1px solid var(--panel-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-header span {
font-size: 10px;
font-weight: 900;
color: var(--sub-text);
text-transform: uppercase;
letter-spacing: 1px;
}
#editor {
flex: 1;
width: 100%;
padding: 20px;
background: transparent;
border: none;
resize: none;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 13px;
color: var(--text-color);
outline: none;
line-height: 1.6;
}
+148
View File
@@ -0,0 +1,148 @@
.sidebar.collapsed .category-header {
display: none;
}
.category-collapsed-icon {
display: none;
width: 100%;
padding: 12px 0;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
border-radius: 12px;
}
.sidebar.collapsed .category-collapsed-icon {
display: flex;
z-index: 10;
}
.category-collapsed-icon:hover {
background: white;
transform: scale(1.1);
}
/* Flyout Panel */
.category-flyout {
position: fixed;
left: 90px;
top: auto;
width: 280px;
max-height: 80vh;
z-index: 9999;
display: flex;
flex-direction: column;
padding: 20px;
opacity: 0;
transform: translateX(-10px);
transition: all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
pointer-events: none;
}
.category-flyout.active {
opacity: 1;
transform: translateX(0);
pointer-events: all;
}
.flyout-menu {
position: absolute;
bottom: 80px;
left: 20px;
width: 220px;
padding: 12px;
z-index: 1000;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
border: 1px solid var(--panel-border);
flex-direction: column;
gap: 2px;
}
.flyout-section {
display: flex;
flex-direction: column;
gap: 2px;
}
.section-label {
font-size: 9px;
font-weight: 800;
color: var(--sub-text);
text-transform: uppercase;
letter-spacing: 1px;
padding: 8px 14px 4px;
opacity: 0.6;
}
.flyout-item {
width: 100%;
padding: 10px 14px;
border: none;
background: transparent;
color: var(--text-color);
font-size: 13px;
font-weight: 600;
text-align: left;
display: flex;
align-items: center;
gap: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.flyout-item i {
width: 16px;
text-align: center;
color: var(--sub-text);
}
.flyout-item:hover {
background: var(--accent-color);
color: white;
}
.flyout-item:hover i {
color: white;
}
.flyout-item.primary {
color: var(--accent-color);
}
.flyout-item.primary:hover {
background: var(--accent-color);
color: white;
}
.divider {
height: 1px;
background: var(--panel-border);
margin: 8px 4px;
}
.flyout-header {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.flyout-header strong {
font-size: 14px;
display: block;
}
.flyout-header small {
font-size: 10px;
color: #94a3b8;
text-transform: uppercase;
}
.flyout-grid {
overflow-y: auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
padding-right: 5px;
}
+139
View File
@@ -0,0 +1,139 @@
.header {
position: absolute;
top: 24px;
right: 24px;
height: 80px;
z-index: 90;
padding: 0 30px;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
}
.header-compact {
height: 50px;
padding: 0 20px;
top: 15px;
right: 24px;
}
.header-compact h2 {
font-size: 13px !important;
}
.header-compact p {
font-size: 9px !important;
}
.header-info {
display: flex;
flex-direction: column;
}
.header-status {
display: flex;
align-items: center;
gap: 12px;
background: rgba(16, 185, 129, 0.1);
padding: 4px 12px;
border-radius: 20px;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.help-circle-btn {
background: transparent;
border: none;
color: #047857;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
padding: 2px;
display: flex;
align-items: center;
justify-content: center;
}
.help-circle-btn:hover {
color: #10b981;
transform: scale(1.1);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.status-dot.online {
background: #10b981;
box-shadow: 0 0 8px #10b981;
}
.header h2 {
font-size: 14px;
font-weight: 800;
}
.header-btns {
display: flex;
gap: 15px;
}
.btn {
padding: 12px 24px;
border-radius: 16px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.btn-secondary {
background: var(--item-bg);
color: var(--sub-text);
}
.btn-primary {
background: var(--accent-color);
color: white;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.header-search {
flex: 1;
max-width: 400px;
margin: 0 40px;
background: rgba(255, 255, 255, 0.2);
border: 1px solid var(--panel-border);
border-radius: 20px;
padding: 6px 15px;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.2s;
}
.header-search:focus-within {
background: rgba(255, 255, 255, 0.4);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.header-search i {
color: var(--sub-text);
font-size: 13px;
}
.header-search input {
background: transparent;
border: none;
outline: none;
color: var(--text-color);
font-size: 12px;
width: 100%;
}
.btn:active {
transform: scale(0.95);
}
+318
View File
@@ -0,0 +1,318 @@
/* layers.css - Layer Management Panel Styles */
.layer-panel {
position: absolute;
bottom: 24px;
right: 300px; /* Offset from Inventory */
width: 250px;
max-height: 400px;
z-index: 1000;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: auto !important;
}
.layer-item.dragging {
opacity: 0.4;
background: var(--primary-color-dim);
border: 1px dashed var(--primary-color);
}
.drag-handle {
cursor: grab;
color: #64748b;
margin-right: 12px;
font-size: 12px;
}
.layer-item:active .drag-handle {
cursor: grabbing;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 13px;
font-weight: 800;
color: var(--text-color);
border-bottom: 1px solid var(--panel-border);
padding-bottom: 10px;
}
.panel-header i {
color: var(--accent-color);
}
.layer-list {
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
}
.layer-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: var(--item-bg);
border: 1px solid var(--panel-border);
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
}
.layer-item:hover {
background: var(--item-hover-bg);
border-color: var(--accent-color);
}
.layer-item.active {
border-color: var(--accent-color);
background: rgba(59, 130, 246, 0.1);
}
.layer-color {
width: 12px;
height: 12px;
border-radius: 50%;
}
.layer-name {
flex: 1;
font-size: 11px;
font-weight: 600;
color: var(--text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.layer-actions {
display: flex;
gap: 4px;
}
.layer-actions button {
background: none;
border: none;
color: var(--sub-text);
cursor: pointer;
font-size: 12px;
padding: 2px;
}
.layer-actions button:hover {
color: var(--accent-color);
}
.panel-header button {
background: var(--accent-color);
color: white !important;
border: none;
border-radius: 4px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.panel-header button:hover {
background: #2563eb;
transform: scale(1.1);
}
.panel-header button i {
color: white !important; /* Force icon color */
}
.mini-btn:hover {
background: #2563eb;
}
.layer-footer {
margin-top: auto;
padding-top: 10px;
border-top: 1px solid var(--panel-border);
}
.opacity-control {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--sub-text);
}
.opacity-control input[type="range"] {
flex: 1;
height: 6px;
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
appearance: none;
cursor: pointer;
outline: none;
}
.opacity-control input[type="range"]::-webkit-slider-runnable-track {
background: linear-gradient(to right, var(--accent-color), #ddd);
height: 6px;
border-radius: 3px;
}
.opacity-control input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 14px;
height: 14px;
background: var(--accent-color);
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
border-radius: 50%;
cursor: pointer;
margin-top: -4px; /* Align with track */
}
#opacity-val {
min-width: 30px;
text-align: right;
}
.layer-rename-input {
background: white;
border: 1px solid var(--accent-color);
border-radius: 4px;
padding: 2px 4px;
font-size: 13px;
width: 100px;
outline: none;
color: var(--main-text);
}
/* Trash Bin for Deletion */
.layer-trash {
margin-top: 12px;
padding: 12px;
border: 1.5px dashed rgba(239, 68, 68, 0.4);
border-radius: 10px;
color: #ef4444;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 11px;
font-weight: 700;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
background: rgba(239, 68, 68, 0.05);
}
.layer-trash i {
font-size: 14px;
}
.layer-trash.drag-over {
background: #ef4444;
color: white;
border-color: #ef4444;
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
/* Visual Nudge: Pulse Animation */
@keyframes nudgePulse {
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); }
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
}
.pulse-nudge {
animation: nudgePulse 1.5s infinite;
border-radius: 4px;
background: rgba(59, 130, 246, 0.1);
}
/* Mini Guide Tooltip */
.nudge-tooltip {
position: absolute;
left: 45px;
background: #3b82f6;
color: white;
padding: 6px 10px;
border-radius: 6px;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
pointer-events: none;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
animation: tooltipFadeIn 0.3s ease-out forwards;
z-index: 1001;
}
.nudge-tooltip::before {
content: '';
position: absolute;
left: -4px;
top: 50%;
transform: translateY(-50%);
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-right: 4px solid #3b82f6;
}
@keyframes tooltipFadeIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes tooltipFadeOut {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(5px); }
}
/* Active Layer Floating Badge */
.active-layer-badge {
position: absolute;
top: 20px;
right: 20px;
z-index: 100;
pointer-events: none;
display: flex;
align-items: center;
gap: 8px;
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(12px);
padding: 8px 16px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
animation: fadeInDown 0.5s ease-out;
}
.active-layer-badge .badge-label {
font-size: 9px;
font-weight: 800;
color: #94a3b8;
letter-spacing: 1px;
}
.active-layer-badge .badge-value {
font-size: 13px;
font-weight: 900;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
transition: all 0.3s ease;
}
@keyframes badgePop {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.badge-pop {
animation: badgePop 0.3s ease-out;
}
+84
View File
@@ -0,0 +1,84 @@
/* Main Area Adjustment */
.header, .editor-panel {
left: 284px; /* Default sidebar width + margin */
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar.collapsed ~ .header,
.sidebar.collapsed ~ .editor-panel,
.sidebar.collapsed ~ .main-viewport {
left: 104px; /* Collapsed sidebar width + margin */
}
.main-viewport {
position: absolute;
top: 0;
left: 284px;
right: 0;
bottom: 0;
overflow: auto;
background-color: var(--bg-color);
display: flex;
/* justify-content and align-items removed to allow margin:auto to handle centering with overflow */
padding: 60px; /* Space for shadow and boundaries */
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
scrollbar-gutter: stable;
}
#cy {
flex-shrink: 0;
background-color: var(--canvas-bg); /* Theme-aware paper background */
position: relative;
/* Sizes will be set by JS or default to 100% */
width: 100%;
height: 100%;
margin: auto; /* Handles centering when sized fixed (A3/A4) */
box-shadow: 0 0 40px rgba(0,0,0,0.1);
border: 1px solid var(--panel-border); /* Visible canvas boundary */
}
#cy.grid-solid {
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: var(--grid-size) var(--grid-size);
}
#cy.grid-dashed {
background-image:
radial-gradient(var(--grid-color) 1px, transparent 1px);
background-size: var(--grid-size) var(--grid-size);
}
/* Inventory Panel Overlay (Legacy - handled in analysis.css) */
/* .inventory-panel { ... } */
.inventory-header {
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
color: var(--sub-text);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.inventory-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
font-size: 12px;
font-weight: 600;
color: var(--text-color);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.inventory-item .count {
background: var(--accent-color);
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 800;
}
+141
View File
@@ -0,0 +1,141 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(4px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.modal-overlay.active {
opacity: 1;
pointer-events: all;
}
.modal-content {
width: 500px;
max-width: 90vw;
background: var(--panel-bg);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid var(--panel-border);
border-radius: 32px;
box-shadow: 0 20px 50px rgba(0,0,0,0.1);
transform: scale(0.9) translateY(20px);
transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28);
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-overlay.active .modal-content {
transform: scale(1) translateY(0);
}
.modal-header {
padding: 24px 30px;
border-bottom: 1px solid var(--panel-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
font-size: 18px;
font-weight: 800;
color: var(--text-color);
}
.close-modal {
background: transparent;
border: none;
font-size: 20px;
color: var(--sub-text);
cursor: pointer;
transition: color 0.2s;
}
.close-modal:hover {
color: var(--text-color);
}
.modal-body {
padding: 30px;
flex: 1;
}
.settings-group {
margin-bottom: 24px;
}
.settings-group h3 {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--sub-text);
margin-bottom: 12px;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
}
.setting-label {
font-size: 14px;
font-weight: 600;
}
.setting-item select,
.setting-item input[type="number"],
.setting-item input[type="text"] {
background: var(--item-bg);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 6px 10px;
font-size: 13px;
color: var(--text-color);
outline: none;
transition: border-color 0.2s;
}
.setting-item select:focus,
.setting-item input:focus {
border-color: var(--accent-color);
}
.setting-item select option {
background-color: var(--bg-color);
color: var(--text-color);
}
.setting-item input[type="color"] {
appearance: none;
width: 40px;
height: 24px;
border: none;
border-radius: 4px;
cursor: pointer;
background: none;
}
.setting-item input[type="color"]::-webkit-color-swatch {
border: 1px solid var(--panel-border);
border-radius: 4px;
}
.modal-footer {
padding: 20px 30px;
border-top: 1px solid var(--panel-border);
display: flex;
justify-content: flex-end;
}
+256
View File
@@ -0,0 +1,256 @@
/* Properties Sidebar Styles */
#properties-sidebar {
position: fixed;
right: -320px; /* Hidden by default */
top: 60px; /* Below header */
bottom: 0;
width: 300px;
background: var(--panel-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-left: 1px solid var(--panel-border);
box-shadow: -10px 0 25px rgba(0, 0, 0, 0.3);
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 900;
display: flex;
flex-direction: column;
color: var(--text-color);
}
#properties-sidebar.open {
right: 0;
}
.sidebar-header {
padding: 18px 20px;
border-bottom: 1px solid var(--panel-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header h3 {
margin: 0;
font-size: 16px;
font-weight: 800;
color: var(--accent-color);
}
.close-sidebar {
cursor: pointer;
font-size: 20px;
color: var(--sub-text);
transition: color 0.2s;
}
.close-sidebar:hover {
color: var(--text-color);
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 24px 20px;
}
.prop-group {
margin-bottom: 20px;
}
.prop-row {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.prop-row .prop-group {
flex: 1;
margin-bottom: 0;
}
.prop-group.horizontal {
display: flex;
align-items: center;
gap: 12px;
}
.prop-group.horizontal label {
flex: 0 0 80px;
margin-bottom: 0;
}
.prop-group.horizontal .prop-input {
flex: 1;
}
.prop-group label {
display: block;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--sub-text);
margin-bottom: 8px;
font-weight: 700;
}
.prop-input {
width: 100%;
background: var(--item-bg);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 9px 12px;
color: var(--text-color);
font-size: 14px;
outline: none;
transition: all 0.2s;
}
.prop-input:focus {
border-color: var(--accent-color);
background: var(--item-hover-bg);
}
.sidebar-footer {
padding: 16px 20px;
border-top: 1px solid var(--panel-border);
display: flex;
gap: 12px;
background: rgba(var(--bg-rgb), 0.05);
}
.btn-apply {
flex: 1;
background: var(--accent-color);
color: white;
border: none;
padding: 12px;
border-radius: 8px;
cursor: pointer;
font-weight: 700;
transition: all 0.2s;
}
.btn-apply:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
/* Toggle Switch Styles */
.toggle-group {
margin-bottom: 24px;
}
.toggle-switch {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 12px 14px;
background: var(--item-bg);
border: 1px solid var(--panel-border);
border-radius: 12px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.toggle-switch:hover {
background: var(--item-hover-bg);
border-color: var(--accent-color);
}
.toggle-switch span {
font-size: 13px;
font-weight: 600;
color: var(--text-color);
}
.toggle-switch input {
display: none;
}
.switch-slider {
position: relative;
width: 36px;
height: 20px;
background: #475569;
border-radius: 20px;
transition: background 0.3s;
}
.switch-slider:before {
content: "";
position: absolute;
width: 16px;
height: 16px;
left: 2px;
top: 2px;
background: white;
border-radius: 50%;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.toggle-switch input:checked + .switch-slider {
background: var(--accent-color);
}
.toggle-switch input:checked + .switch-slider:before {
transform: translateX(16px);
}
/* Multi-Selection Alignment UI */
.alignment-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 12px;
}
.align-btn {
aspect-ratio: 1;
background: var(--item-bg);
border: 1px solid var(--panel-border);
border-radius: 8px;
color: var(--sub-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s;
}
.align-btn:hover:not(:disabled) {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.align-btn:disabled {
opacity: 0.2;
cursor: not-allowed;
}
/* Color Input Consistency */
.input-with-color {
display: flex;
gap: 8px;
align-items: center;
}
.input-with-color .prop-input[type="color"] {
width: 44px;
height: 38px;
padding: 3px;
cursor: pointer;
flex-shrink: 0;
}
.input-with-color .prop-input[type="color"]::-webkit-color-swatch {
border: 1px solid var(--panel-border);
border-radius: 4px;
}
/* Danger variant for toggle-switch */
.toggle-switch.danger input:checked + .switch-slider {
background: #ef4444;
}
+195
View File
@@ -0,0 +1,195 @@
/* settings_panel.css - Professional Sliding Settings Panel */
.settings-panel {
position: fixed;
top: 10px;
right: -420px; /* Hidden by default */
width: 400px;
height: calc(100vh - 20px);
z-index: 2000;
display: flex;
flex-direction: column;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.3);
}
.settings-panel.active {
right: 10px;
}
.settings-header {
padding: 20px 24px;
border-bottom: 1px solid var(--panel-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 16px;
font-weight: 800;
color: var(--text-color);
}
.header-title i {
color: var(--accent-color);
}
.settings-container {
flex: 1;
display: flex;
overflow: hidden;
}
/* Sidebar Tabs */
.settings-sidebar {
width: 100px;
background: var(--item-bg);
border-right: 1px solid var(--panel-border);
display: flex;
flex-direction: column;
padding: 10px 0;
}
.settings-tab {
padding: 18px 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
cursor: pointer;
color: var(--sub-text);
transition: all 0.2s ease;
text-align: center;
}
.settings-tab i {
font-size: 20px;
}
.settings-tab span {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.settings-tab:hover {
color: var(--text-color);
background: var(--item-hover-bg);
}
.settings-tab.active {
color: var(--accent-color);
background: rgba(59, 130, 246, 0.1);
border-left: 3px solid var(--accent-color);
}
/* Body Content */
.settings-body {
flex: 1;
padding: 24px;
overflow-y: auto;
background: rgba(var(--bg-rgb), 0.1);
}
.settings-section {
margin-bottom: 30px;
animation: fadeIn 0.3s ease;
}
.settings-section h3 {
font-size: 11px;
color: var(--sub-text);
text-transform: uppercase;
margin-bottom: 16px;
letter-spacing: 0.8px;
font-weight: 800;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 0;
border-bottom: 1px solid var(--panel-border);
}
.setting-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.setting-info .label {
font-size: 14px;
font-weight: 600;
color: var(--text-color);
}
.setting-info .desc {
font-size: 11px;
color: var(--sub-text);
}
.setting-ctrl select,
.setting-ctrl input[type="number"],
.setting-ctrl input[type="text"] {
background: var(--item-bg);
border: 1px solid var(--panel-border);
color: var(--text-color);
padding: 7px 12px;
border-radius: 8px;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.setting-ctrl select:focus,
.setting-ctrl input:focus {
border-color: var(--accent-color);
}
/* Toggle switch adjustments within settings */
.toggle-switch {
background: var(--item-bg) !important;
border: 1px solid var(--panel-border) !important;
}
.toggle-switch:hover {
background: var(--item-hover-bg) !important;
border-color: var(--accent-color) !important;
}
.toggle-switch .label {
color: var(--sub-text) !important;
}
/* Animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.close-btn {
background: none;
border: none;
color: var(--sub-text);
cursor: pointer;
font-size: 20px;
transition: all 0.2s;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
background: #ef4444;
color: white;
}
+140
View File
@@ -0,0 +1,140 @@
.sidebar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 260px;
z-index: 100;
display: flex;
flex-direction: column;
border-radius: 0;
border-left: none;
border-top: none;
border-bottom: none;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
overflow-x: hidden;
}
.sidebar.collapsed {
width: 80px;
overflow-x: hidden !important;
}
.sidebar-header {
padding: 24px;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 80px;
transition: padding 0.3s;
overflow: hidden;
}
.sidebar.collapsed .sidebar-header {
padding: 24px 0;
flex-direction: column;
gap: 15px;
justify-content: center;
}
.menu-toggle {
background: transparent;
border: none;
color: var(--sub-text);
font-size: 20px;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.2s;
}
.sidebar.collapsed .menu-toggle {
order: -1; /* Toggle button first when collapsed */
}
.menu-toggle:hover {
background: var(--item-bg);
}
.logo-area {
display: flex;
align-items: center;
gap: 12px;
transition: opacity 0.2s;
white-space: nowrap;
}
.sidebar.collapsed .logo-text {
display: none;
}
.logo-icon {
min-width: 40px;
width: 40px;
height: 40px;
background: linear-gradient(135deg, #3b82f6, #6366f1);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 900;
font-size: 18px;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.search-container {
padding: 0 24px 20px;
position: relative;
transition: padding 0.3s;
}
.search-container i {
position: absolute;
left: 40px;
top: 13px;
font-size: 14px;
color: var(--sub-text);
z-index: 5;
transition: all 0.3s;
}
#asset-search {
width: 100%;
background: var(--item-bg);
border: 1px solid var(--panel-border);
padding: 10px 15px 10px 40px;
border-radius: 12px;
font-size: 13px;
font-family: inherit;
outline: none;
transition: all 0.2s;
}
#asset-search:focus {
background: var(--item-hover-bg);
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
border-color: #3b82f6;
}
.sidebar.collapsed .search-container {
padding: 10px 0;
display: flex;
justify-content: center;
}
.sidebar.collapsed #asset-search {
display: none;
}
.sidebar.collapsed .search-container i {
font-size: 18px;
color: var(--sub-text);
cursor: pointer;
position: static;
}
+68
View File
@@ -0,0 +1,68 @@
/* Sidebar Footer Settings */
.sidebar-footer {
padding: 20px 24px;
border-top: 1px solid var(--panel-border);
display: flex;
align-items: center;
gap: 12px;
margin-top: auto; /* Push to bottom */
}
.footer-btns {
display: flex;
gap: 8px;
}
.footer-btn {
width: 32px;
height: 32px;
background: var(--item-bg);
border: none;
border-radius: 8px;
color: var(--sub-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.footer-btn:hover {
background: var(--item-hover-bg);
color: white;
}
#settings-btn:hover {
transform: rotate(45deg);
}
#system-menu-btn.active {
background: var(--accent-color);
color: white;
}
.footer-text {
flex: 1;
}
.footer-text p {
font-size: 11px;
font-weight: 800;
margin: 0;
}
.footer-text small {
font-size: 9px;
color: var(--sub-text);
}
.sidebar.collapsed .sidebar-footer {
padding: 20px 0;
flex-direction: column;
justify-content: center;
}
.sidebar.collapsed .footer-text {
display: none;
}
+99
View File
@@ -0,0 +1,99 @@
.floating-window {
position: fixed;
background: var(--sidebar-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--panel-border);
border-radius: 16px;
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
overflow: hidden;
animation: win-appear 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes win-appear {
from { opacity: 0; transform: scale(0.9) translateY(20px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.win-header {
padding: 12px 20px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid var(--panel-border);
display: flex;
align-items: center;
justify-content: space-between;
cursor: grab;
user-select: none;
}
.win-header:active {
cursor: grabbing;
}
.win-title {
font-size: 13px;
font-weight: 800;
color: var(--text-color);
display: flex;
align-items: center;
gap: 10px;
}
.win-controls {
display: flex;
gap: 10px;
}
.win-btn {
background: transparent;
border: none;
color: var(--sub-text);
cursor: pointer;
font-size: 12px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s;
}
.win-btn:hover {
background: var(--item-hover-bg);
color: var(--text-color);
}
.win-btn.win-close:hover {
background: #ef4444;
color: white;
}
.floating-window.minimized {
height: 48px !important;
width: 250px !important;
}
.floating-window.minimized .win-body {
display: none;
}
.win-body {
flex: 1;
overflow: auto;
padding: 20px;
}
/* Studio Specific Polish */
.studio-container {
height: 100%;
}
.studio-modal .preview-area {
min-height: 200px;
}
[data-theme='dark'] .floating-window {
background: rgba(15, 23, 42, 0.8);
}
+190
View File
@@ -0,0 +1,190 @@
/*
drawNET Premium CSS Manifest
This file imports modules to keep the production bundle organized.
*/
@import "modules/base.css";
@import "modules/sidebar.css";
@import "modules/sidebar_footer.css";
@import "modules/assets.css";
@import "modules/flyout.css";
@import "modules/header.css";
@import "modules/editor.css";
@import "modules/layout.css";
@import "modules/animations.css";
@import "modules/modal.css";
@import "modules/settings_panel.css";
@import "modules/properties_sidebar.css";
/* Floating Context Menu */
.floating-menu {
background: rgba(30, 41, 59, 0.85); /* Slate 800-ish with glassmorphism */
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
padding: 6px;
min-width: 180px;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.menu-item {
padding: 10px 14px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
color: #f1f5f9;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
}
.menu-item:hover {
background: #3b82f6; /* Blue 500 */
color: white;
transform: translateY(-1px);
}
.menu-divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
margin: 6px 0;
}
.menu-label {
padding: 6px 14px 4px;
font-size: 10px;
font-weight: 700;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.menu-icon-row {
display: flex;
justify-content: space-between;
padding: 4px 8px;
gap: 4px;
}
.icon-btn {
flex: 1;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
color: #94a3b8;
transition: all 0.2s;
font-size: 14px;
}
.icon-btn:hover {
background: #3b82f6;
color: white;
}
.icon-btn i.fa-rotate-90 {
transform: rotate(90deg);
}
/* --- X6 Port Styles --- */
.x6-port {
visibility: hidden;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.x6-node:hover .x6-port {
visibility: visible;
}
.x6-port circle {
cursor: crosshair;
}
.x6-port circle:hover {
fill: #3b82f6 !important;
r: 6 !important;
}
/* --- X6 Edge Selection Highlight --- */
.x6-edge-selected path {
stroke: #3b82f6 !important;
stroke-width: 3px !important;
filter: drop-shadow(0 0 3px rgba(59, 130, 246, 0.8));
transition: all 0.2s ease;
}
/* Flow animation for selected flow lines */
.x6-edge-selected.flow-animation path {
stroke-width: 4px !important;
}
/* Mini buttons for editor actions */
.footer-btn.mini {
width: auto;
padding: 6px 12px;
font-size: 11px;
height: 28px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
color: #3b82f6;
border-radius: 6px;
margin-left: 6px;
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 700;
}
.footer-btn.mini:hover {
background: #3b82f6;
color: white;
}
/* --- Export Loading Overlay --- */
.export-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(15, 23, 42, 0.65); /* Slate 900 with transparency */
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
color: white;
font-family: inherit;
animation: fadeIn 0.3s ease-out;
}
.export-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-left-color: #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
.export-text {
font-size: 18px;
font-weight: 700;
letter-spacing: -0.02em;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
+344
View File
@@ -0,0 +1,344 @@
/*
* drawNET Studio - Asset Management Styles
*/
body.studio-body {
margin: 0;
padding: 0;
background: #0f172a;
color: #f8fafc;
height: 100vh;
overflow: hidden;
font-family: 'Inter', sans-serif;
}
.studio-layout {
display: grid;
grid-template-columns: 280px 1fr 320px;
grid-template-rows: 60px 1fr;
height: 100vh;
}
.studio-header {
grid-column: 1 / 4;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: rgba(30, 41, 59, 0.7);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
}
.studio-header-left {
display: flex;
align-items: center;
gap: 20px;
}
.header-metadata {
display: flex;
gap: 15px;
align-items: center;
}
.meta-input-group {
display: flex;
flex-direction: column;
}
.meta-label {
font-size: 9px;
color: #64748b;
font-weight: 800;
text-transform: uppercase;
}
.meta-input {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #f8fafc;
padding: 4px 8px;
border-radius: 4px;
font-size: 13px;
transition: border-color 0.2s;
}
.meta-input:focus {
outline: none;
border-color: #38bdf8;
}
.meta-input.highlight {
color: #38bdf8;
font-weight: 700;
}
.panel {
background: rgba(15, 23, 42, 0.5);
border-right: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
padding: 15px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 12px;
font-weight: 800;
color: #94a3b8;
letter-spacing: 0.05em;
text-transform: uppercase;
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
/* Source Panel (Left) */
.source-panel {
background: rgba(15, 23, 42, 0.8);
}
.upload-area {
border: 2px dashed rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 30px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 20px;
}
.upload-area:hover {
background: rgba(255, 255, 255, 0.02);
border-color: #38bdf8;
}
.upload-area i {
display: block;
font-size: 24px;
color: #38bdf8;
margin-bottom: 10px;
}
.upload-text {
font-size: 12px;
margin: 0;
}
.upload-hint {
font-size: 10px;
color: #64748b;
}
.source-list {
list-style: none;
padding: 0;
margin: 0;
}
.source-item {
padding: 10px 12px;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: background 0.2s;
}
.source-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.source-item i {
width: 16px;
opacity: 0.6;
}
/* Workspace (Center) */
.workspace {
background: #020617;
padding: 30px;
}
.asset-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
}
.asset-card {
background: rgba(30, 41, 59, 0.5);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 15px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
cursor: pointer;
transition: all 0.2s;
}
.asset-card:hover {
transform: translateY(-2px);
background: rgba(30, 41, 59, 0.8);
border-color: #38bdf8;
}
.asset-card.selected {
border-color: #38bdf8;
background: rgba(56, 189, 248, 0.1);
box-shadow: 0 0 0 1px #38bdf8;
}
.asset-preview {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
}
.asset-preview img {
max-width: 100%;
max-height: 100%;
}
.asset-name {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
text-align: center;
color: #cbd5e1;
}
/* Comparison View */
.comparison-view {
display: flex;
gap: 20px;
margin-top: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.comp-box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.comp-preview {
width: 100px;
height: 100px;
background: #1e293b;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.comp-preview img, .comp-preview svg {
max-width: 90%;
max-height: 90%;
}
.comp-label {
font-size: 10px;
font-weight: 800;
color: #64748b;
text-transform: uppercase;
}
.choice-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #94a3b8;
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
cursor: pointer;
width: 100%;
transition: all 0.2s;
}
.choice-btn.active {
background: #38bdf8;
color: #0f172a;
border-color: #38bdf8;
font-weight: 700;
}
/* Property Panel (Right) */
.property-panel {
border-left: 1px solid rgba(255, 255, 255, 0.05);
background: rgba(15, 23, 42, 0.8);
}
.property-empty {
text-align: center;
color: #64748b;
margin-top: 50px;
}
.property-empty i {
font-size: 30px;
margin-bottom: 15px;
display: block;
}
.property-empty p {
font-size: 13px;
}
.btn-primary {
background: #38bdf8;
color: #0f172a;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-weight: 700;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover {
background: #7dd3fc;
transform: translateY(-1px);
}
.back-link {
text-decoration: none;
color: #94a3b8;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
}
.back-link:hover {
color: #cbd5e1;
}
.placeholder-icon {
font-size: 32px;
color: #334155;
}
+1
View File
@@ -0,0 +1 @@
/* Ghost file to prevent MIME type 404 errors in legacy/library environments */
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

+54
View File
@@ -0,0 +1,54 @@
{
"hotkeys": [
{ "key": "Delete", "code": "Delete", "action": "deleteSelected", "description": "Remove selected elements" },
{ "key": "Backspace", "code": "Backspace", "action": "deleteSelected", "description": "Remove selected elements" },
{ "ctrl": true, "key": "f", "code": "KeyF", "action": "fitScreen", "description": "Fit to screen" },
{ "ctrl": true, "key": "=", "code": "Equal", "action": "zoomIn", "description": "Zoom in" },
{ "ctrl": true, "key": "+", "code": "Equal", "shift": true, "action": "zoomIn", "description": "Zoom in" },
{ "ctrl": true, "key": "-", "code": "Minus", "action": "zoomOut", "description": "Zoom out" },
{ "ctrl": true, "key": "s", "code": "KeyS", "action": "exportJson", "description": "Export JSON" },
{ "ctrl": true, "key": "o", "code": "KeyO", "action": "importJson", "description": "Import JSON" },
{ "alt": true, "key": "0", "code": "Digit0", "alwaysEnabled": true, "action": "toggleInventory", "description": "Toggle Inventory Panel" },
{ "alt": true, "key": "1", "code": "Digit1", "alwaysEnabled": true, "action": "toggleProperties", "description": "Toggle Properties Sidebar" },
{ "alt": true, "key": "Enter", "code": "Enter", "alwaysEnabled": true, "action": "toggleProperties", "description": "Open/Toggle Properties Sidebar" },
{ "key": "F2", "code": "F2", "action": "editLabel", "description": "Edit label of selected element" },
{ "key": "[", "code": "BracketLeft", "action": "sendToBack", "description": "Send selected to back" },
{ "key": "]", "code": "BracketRight", "action": "bringToFront", "description": "Bring selected to front" },
{ "ctrl": true, "key": "l", "code": "KeyL", "action": "toggleLock", "description": "Lock/Unlock selected element" },
{ "alt": true, "key": "9", "code": "Digit9", "alwaysEnabled": true, "action": "toggleLayers", "description": "Toggle Layer Panel" },
{ "ctrl": true, "shift": true, "key": "l", "code": "KeyL", "action": "arrangeLayout", "description": "Auto-arrange layout" },
{ "shift": true, "key": "a", "code": "KeyA", "action": "connectNodes", "description": "Connect selected nodes (Manhattan)" },
{ "shift": true, "key": "s", "code": "KeyS", "action": "connectStraight", "description": "Connect selected nodes (Straight)" },
{ "alt": true, "key": "m", "code": "KeyM", "action": "connectNodes", "description": "Change to Manhattan Line" },
{ "alt": true, "key": "l", "code": "KeyL", "action": "connectStraight", "description": "Change to Straight Line" },
{ "shift": true, "key": "d", "code": "KeyD", "action": "disconnectNodes", "description": "Disconnect selected nodes" },
{ "ctrl": true, "key": "g", "code": "KeyG", "action": "groupNodes", "description": "Group Selected" },
{ "ctrl": true, "key": "z", "code": "KeyZ", "action": "undo", "alwaysEnabled": true, "description": "Undo last action" },
{ "ctrl": true, "key": "y", "code": "KeyY", "action": "redo", "alwaysEnabled": true, "description": "Redo last undone action" },
{ "ctrl": true, "shift": true, "key": "z", "code": "KeyZ", "action": "redo", "alwaysEnabled": true, "description": "Redo last undone action" },
{ "ctrl": true, "key": "c", "code": "KeyC", "action": "copyNodes", "description": "Copy selected nodes" },
{ "ctrl": true, "key": "v", "code": "KeyV", "action": "pasteNodes", "description": "Paste nodes from clipboard" },
{ "ctrl": true, "key": "d", "code": "KeyD", "action": "duplicateNodes", "description": "Duplicate selected nodes (Immediate)" },
{ "ctrl": true, "shift": true, "key": "c", "code": "KeyC", "action": "copyAttributes", "description": "Copy node attributes (Format Painter)" },
{ "ctrl": true, "shift": true, "key": "v", "code": "KeyV", "action": "pasteAttributes", "description": "Paste node attributes" },
{ "shift": true, "key": "1", "code": "Digit1", "action": "alignTop", "description": "Align nodes to top" },
{ "shift": true, "key": "2", "code": "Digit2", "action": "alignBottom", "description": "Align nodes to bottom" },
{ "shift": true, "key": "3", "code": "Digit3", "action": "alignLeft", "description": "Align nodes to left" },
{ "shift": true, "key": "4", "code": "Digit4", "action": "alignRight", "description": "Align nodes to right" },
{ "shift": true, "key": "5", "code": "Digit5", "action": "alignMiddle", "description": "Align nodes to vertical center" },
{ "shift": true, "key": "6", "code": "Digit6", "action": "alignCenter", "description": "Align nodes to horizontal center" },
{ "shift": true, "key": "7", "code": "Digit7", "action": "distributeHorizontal", "description": "Distribute nodes horizontally" },
{ "shift": true, "key": "8", "code": "Digit8", "action": "distributeVertical", "description": "Distribute nodes vertically" },
{ "key": "ArrowUp", "code": "ArrowUp", "action": "moveUp", "description": "Move nodes up" },
{ "key": "ArrowDown", "code": "ArrowDown", "action": "moveDown", "description": "Move nodes down" },
{ "key": "ArrowLeft", "code": "ArrowLeft", "action": "moveLeft", "description": "Move nodes left" },
{ "key": "ArrowRight", "code": "ArrowRight", "action": "moveRight", "description": "Move nodes right" },
{ "shift": true, "key": "ArrowUp", "code": "ArrowUp", "action": "moveUpLarge", "description": "Move nodes up (large)" },
{ "shift": true, "key": "ArrowDown", "code": "ArrowDown", "action": "moveDownLarge", "description": "Move nodes down (large)" },
{ "shift": true, "key": "ArrowLeft", "code": "ArrowLeft", "action": "moveLeftLarge", "description": "Move nodes left (large)" },
{ "shift": true, "key": "ArrowRight", "code": "ArrowRight", "action": "moveRightLarge", "description": "Move nodes right (large)" },
{ "key": "Escape", "code": "Escape", "alwaysEnabled": true, "action": "cancel", "description": "Deselect all and close sidebar" }
]
}
+76
View File
@@ -0,0 +1,76 @@
import { initAssets } from './modules/assets/index.js';
import { initGraph } from './modules/graph.js';
import { initUIToggles } from './modules/ui.js';
import { initSettings } from './modules/settings/ui.js';
import { applySavedSettings, applySavedTheme } from './modules/settings/store.js';
import { initHotkeys } from './modules/hotkeys/index.js';
import { initGlobalSearch } from './modules/graph/search.js';
import { initInventory } from './modules/graph/analysis.js';
import { initLayerPanel } from './modules/ui/layer_panel.js';
import { applyLayerFilters } from './modules/graph/layers.js';
import { initI18n } from './modules/i18n.js';
import { initPropertiesSidebar } from './modules/properties_sidebar/index.js';
import { initPersistence } from './modules/persistence.js';
import { initGraphIO } from './modules/graph/io/index.js';
import { initContextMenu } from './modules/ui/context_menu/index.js';
import { logger } from './modules/utils/logger.js';
document.addEventListener('DOMContentLoaded', async () => {
// 0. Initialize i18n (Language first)
await initI18n();
// 1. Apply Saved Theme immediately (prevent flash)
applySavedTheme();
// 2. Load Assets & Sidebar
await initAssets();
// 2.5 Load Global Config
try {
const configRes = await fetch('/api/config');
if (configRes.ok) {
const configData = await configRes.json();
const { state } = await import('./modules/state.js');
state.appConfig = configData;
logger.info("Global configuration loaded from server.");
}
} catch (err) {
logger.high("Failed to load global config, using defaults.");
}
// 3. Initialize X6 Graph
initGraph();
// 4. Initialize UI Toggles
initUIToggles();
// 5. Initialize Settings Modal Logic
initSettings();
// 6. Apply Saved Visual Settings (Grid, Size)
applySavedSettings();
// 7. Initialize Keyboard Hotkeys
initHotkeys();
// 8. Initialize UI Components & Persistence
initPropertiesSidebar();
initContextMenu();
initGraphIO();
initGlobalSearch();
initInventory();
initLayerPanel();
applyLayerFilters();
initPersistence();
// Help Button
const helpBtn = document.getElementById('help-btn');
if (helpBtn) {
helpBtn.addEventListener('click', async () => {
const { showHelpModal } = await import('./modules/ui/help_modal.js');
showHelpModal();
});
}
logger.high("drawNET Premium initialized successfully.");
});
+398
View File
File diff suppressed because one or more lines are too long
+140
View File
@@ -0,0 +1,140 @@
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
(function(){function n(n,t,r){switch(r.length){case 0:return n.call(t);case 1:return n.call(t,r[0]);case 2:return n.call(t,r[0],r[1]);case 3:return n.call(t,r[0],r[1],r[2])}return n.apply(t,r)}function t(n,t,r,e){for(var u=-1,i=null==n?0:n.length;++u<i;){var o=n[u];t(e,o,r(o),n)}return e}function r(n,t){for(var r=-1,e=null==n?0:n.length;++r<e&&t(n[r],r,n)!==!1;);return n}function e(n,t){for(var r=null==n?0:n.length;r--&&t(n[r],r,n)!==!1;);return n}function u(n,t){for(var r=-1,e=null==n?0:n.length;++r<e;)if(!t(n[r],r,n))return!1;
return!0}function i(n,t){for(var r=-1,e=null==n?0:n.length,u=0,i=[];++r<e;){var o=n[r];t(o,r,n)&&(i[u++]=o)}return i}function o(n,t){return!!(null==n?0:n.length)&&y(n,t,0)>-1}function f(n,t,r){for(var e=-1,u=null==n?0:n.length;++e<u;)if(r(t,n[e]))return!0;return!1}function c(n,t){for(var r=-1,e=null==n?0:n.length,u=Array(e);++r<e;)u[r]=t(n[r],r,n);return u}function a(n,t){for(var r=-1,e=t.length,u=n.length;++r<e;)n[u+r]=t[r];return n}function l(n,t,r,e){var u=-1,i=null==n?0:n.length;for(e&&i&&(r=n[++u]);++u<i;)r=t(r,n[u],u,n);
return r}function s(n,t,r,e){var u=null==n?0:n.length;for(e&&u&&(r=n[--u]);u--;)r=t(r,n[u],u,n);return r}function h(n,t){for(var r=-1,e=null==n?0:n.length;++r<e;)if(t(n[r],r,n))return!0;return!1}function p(n){return n.split("")}function _(n){return n.match($t)||[]}function v(n,t,r){var e;return r(n,function(n,r,u){if(t(n,r,u))return e=r,!1}),e}function g(n,t,r,e){for(var u=n.length,i=r+(e?1:-1);e?i--:++i<u;)if(t(n[i],i,n))return i;return-1}function y(n,t,r){return t===t?Z(n,t,r):g(n,b,r)}function d(n,t,r,e){
for(var u=r-1,i=n.length;++u<i;)if(e(n[u],t))return u;return-1}function b(n){return n!==n}function w(n,t){var r=null==n?0:n.length;return r?k(n,t)/r:Cn}function m(n){return function(t){return null==t?X:t[n]}}function x(n){return function(t){return null==n?X:n[t]}}function j(n,t,r,e,u){return u(n,function(n,u,i){r=e?(e=!1,n):t(r,n,u,i)}),r}function A(n,t){var r=n.length;for(n.sort(t);r--;)n[r]=n[r].value;return n}function k(n,t){for(var r,e=-1,u=n.length;++e<u;){var i=t(n[e]);i!==X&&(r=r===X?i:r+i);
}return r}function O(n,t){for(var r=-1,e=Array(n);++r<n;)e[r]=t(r);return e}function I(n,t){return c(t,function(t){return[t,n[t]]})}function R(n){return n?n.slice(0,H(n)+1).replace(Lt,""):n}function z(n){return function(t){return n(t)}}function E(n,t){return c(t,function(t){return n[t]})}function S(n,t){return n.has(t)}function W(n,t){for(var r=-1,e=n.length;++r<e&&y(t,n[r],0)>-1;);return r}function L(n,t){for(var r=n.length;r--&&y(t,n[r],0)>-1;);return r}function C(n,t){for(var r=n.length,e=0;r--;)n[r]===t&&++e;
return e}function U(n){return"\\"+Yr[n]}function B(n,t){return null==n?X:n[t]}function T(n){return Nr.test(n)}function $(n){return Pr.test(n)}function D(n){for(var t,r=[];!(t=n.next()).done;)r.push(t.value);return r}function M(n){var t=-1,r=Array(n.size);return n.forEach(function(n,e){r[++t]=[e,n]}),r}function F(n,t){return function(r){return n(t(r))}}function N(n,t){for(var r=-1,e=n.length,u=0,i=[];++r<e;){var o=n[r];o!==t&&o!==cn||(n[r]=cn,i[u++]=r)}return i}function P(n){var t=-1,r=Array(n.size);
return n.forEach(function(n){r[++t]=n}),r}function q(n){var t=-1,r=Array(n.size);return n.forEach(function(n){r[++t]=[n,n]}),r}function Z(n,t,r){for(var e=r-1,u=n.length;++e<u;)if(n[e]===t)return e;return-1}function K(n,t,r){for(var e=r+1;e--;)if(n[e]===t)return e;return e}function V(n){return T(n)?J(n):_e(n)}function G(n){return T(n)?Y(n):p(n)}function H(n){for(var t=n.length;t--&&Ct.test(n.charAt(t)););return t}function J(n){for(var t=Mr.lastIndex=0;Mr.test(n);)++t;return t}function Y(n){return n.match(Mr)||[];
}function Q(n){return n.match(Fr)||[]}var X,nn="4.17.21",tn=200,rn="Unsupported core-js use. Try https://npms.io/search?q=ponyfill.",en="Expected a function",un="Invalid `variable` option passed into `_.template`",on="__lodash_hash_undefined__",fn=500,cn="__lodash_placeholder__",an=1,ln=2,sn=4,hn=1,pn=2,_n=1,vn=2,gn=4,yn=8,dn=16,bn=32,wn=64,mn=128,xn=256,jn=512,An=30,kn="...",On=800,In=16,Rn=1,zn=2,En=3,Sn=1/0,Wn=9007199254740991,Ln=1.7976931348623157e308,Cn=NaN,Un=4294967295,Bn=Un-1,Tn=Un>>>1,$n=[["ary",mn],["bind",_n],["bindKey",vn],["curry",yn],["curryRight",dn],["flip",jn],["partial",bn],["partialRight",wn],["rearg",xn]],Dn="[object Arguments]",Mn="[object Array]",Fn="[object AsyncFunction]",Nn="[object Boolean]",Pn="[object Date]",qn="[object DOMException]",Zn="[object Error]",Kn="[object Function]",Vn="[object GeneratorFunction]",Gn="[object Map]",Hn="[object Number]",Jn="[object Null]",Yn="[object Object]",Qn="[object Promise]",Xn="[object Proxy]",nt="[object RegExp]",tt="[object Set]",rt="[object String]",et="[object Symbol]",ut="[object Undefined]",it="[object WeakMap]",ot="[object WeakSet]",ft="[object ArrayBuffer]",ct="[object DataView]",at="[object Float32Array]",lt="[object Float64Array]",st="[object Int8Array]",ht="[object Int16Array]",pt="[object Int32Array]",_t="[object Uint8Array]",vt="[object Uint8ClampedArray]",gt="[object Uint16Array]",yt="[object Uint32Array]",dt=/\b__p \+= '';/g,bt=/\b(__p \+=) '' \+/g,wt=/(__e\(.*?\)|\b__t\)) \+\n'';/g,mt=/&(?:amp|lt|gt|quot|#39);/g,xt=/[&<>"']/g,jt=RegExp(mt.source),At=RegExp(xt.source),kt=/<%-([\s\S]+?)%>/g,Ot=/<%([\s\S]+?)%>/g,It=/<%=([\s\S]+?)%>/g,Rt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,zt=/^\w*$/,Et=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,St=/[\\^$.*+?()[\]{}|]/g,Wt=RegExp(St.source),Lt=/^\s+/,Ct=/\s/,Ut=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,Bt=/\{\n\/\* \[wrapped with (.+)\] \*/,Tt=/,? & /,$t=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,Dt=/[()=,{}\[\]\/\s]/,Mt=/\\(\\)?/g,Ft=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Nt=/\w*$/,Pt=/^[-+]0x[0-9a-f]+$/i,qt=/^0b[01]+$/i,Zt=/^\[object .+?Constructor\]$/,Kt=/^0o[0-7]+$/i,Vt=/^(?:0|[1-9]\d*)$/,Gt=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,Ht=/($^)/,Jt=/['\n\r\u2028\u2029\\]/g,Yt="\\ud800-\\udfff",Qt="\\u0300-\\u036f",Xt="\\ufe20-\\ufe2f",nr="\\u20d0-\\u20ff",tr=Qt+Xt+nr,rr="\\u2700-\\u27bf",er="a-z\\xdf-\\xf6\\xf8-\\xff",ur="\\xac\\xb1\\xd7\\xf7",ir="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",or="\\u2000-\\u206f",fr=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",cr="A-Z\\xc0-\\xd6\\xd8-\\xde",ar="\\ufe0e\\ufe0f",lr=ur+ir+or+fr,sr="['\u2019]",hr="["+Yt+"]",pr="["+lr+"]",_r="["+tr+"]",vr="\\d+",gr="["+rr+"]",yr="["+er+"]",dr="[^"+Yt+lr+vr+rr+er+cr+"]",br="\\ud83c[\\udffb-\\udfff]",wr="(?:"+_r+"|"+br+")",mr="[^"+Yt+"]",xr="(?:\\ud83c[\\udde6-\\uddff]){2}",jr="[\\ud800-\\udbff][\\udc00-\\udfff]",Ar="["+cr+"]",kr="\\u200d",Or="(?:"+yr+"|"+dr+")",Ir="(?:"+Ar+"|"+dr+")",Rr="(?:"+sr+"(?:d|ll|m|re|s|t|ve))?",zr="(?:"+sr+"(?:D|LL|M|RE|S|T|VE))?",Er=wr+"?",Sr="["+ar+"]?",Wr="(?:"+kr+"(?:"+[mr,xr,jr].join("|")+")"+Sr+Er+")*",Lr="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Cr="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",Ur=Sr+Er+Wr,Br="(?:"+[gr,xr,jr].join("|")+")"+Ur,Tr="(?:"+[mr+_r+"?",_r,xr,jr,hr].join("|")+")",$r=RegExp(sr,"g"),Dr=RegExp(_r,"g"),Mr=RegExp(br+"(?="+br+")|"+Tr+Ur,"g"),Fr=RegExp([Ar+"?"+yr+"+"+Rr+"(?="+[pr,Ar,"$"].join("|")+")",Ir+"+"+zr+"(?="+[pr,Ar+Or,"$"].join("|")+")",Ar+"?"+Or+"+"+Rr,Ar+"+"+zr,Cr,Lr,vr,Br].join("|"),"g"),Nr=RegExp("["+kr+Yt+tr+ar+"]"),Pr=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,qr=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],Zr=-1,Kr={};
Kr[at]=Kr[lt]=Kr[st]=Kr[ht]=Kr[pt]=Kr[_t]=Kr[vt]=Kr[gt]=Kr[yt]=!0,Kr[Dn]=Kr[Mn]=Kr[ft]=Kr[Nn]=Kr[ct]=Kr[Pn]=Kr[Zn]=Kr[Kn]=Kr[Gn]=Kr[Hn]=Kr[Yn]=Kr[nt]=Kr[tt]=Kr[rt]=Kr[it]=!1;var Vr={};Vr[Dn]=Vr[Mn]=Vr[ft]=Vr[ct]=Vr[Nn]=Vr[Pn]=Vr[at]=Vr[lt]=Vr[st]=Vr[ht]=Vr[pt]=Vr[Gn]=Vr[Hn]=Vr[Yn]=Vr[nt]=Vr[tt]=Vr[rt]=Vr[et]=Vr[_t]=Vr[vt]=Vr[gt]=Vr[yt]=!0,Vr[Zn]=Vr[Kn]=Vr[it]=!1;var Gr={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a",
"\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae",
"\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g",
"\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O",
"\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w",
"\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"},Hr={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},Jr={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&#39;":"'"},Yr={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Qr=parseFloat,Xr=parseInt,ne="object"==typeof global&&global&&global.Object===Object&&global,te="object"==typeof self&&self&&self.Object===Object&&self,re=ne||te||Function("return this")(),ee="object"==typeof exports&&exports&&!exports.nodeType&&exports,ue=ee&&"object"==typeof module&&module&&!module.nodeType&&module,ie=ue&&ue.exports===ee,oe=ie&&ne.process,fe=function(){
try{var n=ue&&ue.require&&ue.require("util").types;return n?n:oe&&oe.binding&&oe.binding("util")}catch(n){}}(),ce=fe&&fe.isArrayBuffer,ae=fe&&fe.isDate,le=fe&&fe.isMap,se=fe&&fe.isRegExp,he=fe&&fe.isSet,pe=fe&&fe.isTypedArray,_e=m("length"),ve=x(Gr),ge=x(Hr),ye=x(Jr),de=function p(x){function Z(n){if(cc(n)&&!bh(n)&&!(n instanceof Ct)){if(n instanceof Y)return n;if(bl.call(n,"__wrapped__"))return eo(n)}return new Y(n)}function J(){}function Y(n,t){this.__wrapped__=n,this.__actions__=[],this.__chain__=!!t,
this.__index__=0,this.__values__=X}function Ct(n){this.__wrapped__=n,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=Un,this.__views__=[]}function $t(){var n=new Ct(this.__wrapped__);return n.__actions__=Tu(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=Tu(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=Tu(this.__views__),n}function Yt(){if(this.__filtered__){var n=new Ct(this);n.__dir__=-1,
n.__filtered__=!0}else n=this.clone(),n.__dir__*=-1;return n}function Qt(){var n=this.__wrapped__.value(),t=this.__dir__,r=bh(n),e=t<0,u=r?n.length:0,i=Oi(0,u,this.__views__),o=i.start,f=i.end,c=f-o,a=e?f:o-1,l=this.__iteratees__,s=l.length,h=0,p=Hl(c,this.__takeCount__);if(!r||!e&&u==c&&p==c)return wu(n,this.__actions__);var _=[];n:for(;c--&&h<p;){a+=t;for(var v=-1,g=n[a];++v<s;){var y=l[v],d=y.iteratee,b=y.type,w=d(g);if(b==zn)g=w;else if(!w){if(b==Rn)continue n;break n}}_[h++]=g}return _}function Xt(n){
var t=-1,r=null==n?0:n.length;for(this.clear();++t<r;){var e=n[t];this.set(e[0],e[1])}}function nr(){this.__data__=is?is(null):{},this.size=0}function tr(n){var t=this.has(n)&&delete this.__data__[n];return this.size-=t?1:0,t}function rr(n){var t=this.__data__;if(is){var r=t[n];return r===on?X:r}return bl.call(t,n)?t[n]:X}function er(n){var t=this.__data__;return is?t[n]!==X:bl.call(t,n)}function ur(n,t){var r=this.__data__;return this.size+=this.has(n)?0:1,r[n]=is&&t===X?on:t,this}function ir(n){
var t=-1,r=null==n?0:n.length;for(this.clear();++t<r;){var e=n[t];this.set(e[0],e[1])}}function or(){this.__data__=[],this.size=0}function fr(n){var t=this.__data__,r=Wr(t,n);return!(r<0)&&(r==t.length-1?t.pop():Ll.call(t,r,1),--this.size,!0)}function cr(n){var t=this.__data__,r=Wr(t,n);return r<0?X:t[r][1]}function ar(n){return Wr(this.__data__,n)>-1}function lr(n,t){var r=this.__data__,e=Wr(r,n);return e<0?(++this.size,r.push([n,t])):r[e][1]=t,this}function sr(n){var t=-1,r=null==n?0:n.length;for(this.clear();++t<r;){
var e=n[t];this.set(e[0],e[1])}}function hr(){this.size=0,this.__data__={hash:new Xt,map:new(ts||ir),string:new Xt}}function pr(n){var t=xi(this,n).delete(n);return this.size-=t?1:0,t}function _r(n){return xi(this,n).get(n)}function vr(n){return xi(this,n).has(n)}function gr(n,t){var r=xi(this,n),e=r.size;return r.set(n,t),this.size+=r.size==e?0:1,this}function yr(n){var t=-1,r=null==n?0:n.length;for(this.__data__=new sr;++t<r;)this.add(n[t])}function dr(n){return this.__data__.set(n,on),this}function br(n){
return this.__data__.has(n)}function wr(n){this.size=(this.__data__=new ir(n)).size}function mr(){this.__data__=new ir,this.size=0}function xr(n){var t=this.__data__,r=t.delete(n);return this.size=t.size,r}function jr(n){return this.__data__.get(n)}function Ar(n){return this.__data__.has(n)}function kr(n,t){var r=this.__data__;if(r instanceof ir){var e=r.__data__;if(!ts||e.length<tn-1)return e.push([n,t]),this.size=++r.size,this;r=this.__data__=new sr(e)}return r.set(n,t),this.size=r.size,this}function Or(n,t){
var r=bh(n),e=!r&&dh(n),u=!r&&!e&&mh(n),i=!r&&!e&&!u&&Oh(n),o=r||e||u||i,f=o?O(n.length,hl):[],c=f.length;for(var a in n)!t&&!bl.call(n,a)||o&&("length"==a||u&&("offset"==a||"parent"==a)||i&&("buffer"==a||"byteLength"==a||"byteOffset"==a)||Ci(a,c))||f.push(a);return f}function Ir(n){var t=n.length;return t?n[tu(0,t-1)]:X}function Rr(n,t){return Xi(Tu(n),Mr(t,0,n.length))}function zr(n){return Xi(Tu(n))}function Er(n,t,r){(r===X||Gf(n[t],r))&&(r!==X||t in n)||Br(n,t,r)}function Sr(n,t,r){var e=n[t];
bl.call(n,t)&&Gf(e,r)&&(r!==X||t in n)||Br(n,t,r)}function Wr(n,t){for(var r=n.length;r--;)if(Gf(n[r][0],t))return r;return-1}function Lr(n,t,r,e){return ys(n,function(n,u,i){t(e,n,r(n),i)}),e}function Cr(n,t){return n&&$u(t,Pc(t),n)}function Ur(n,t){return n&&$u(t,qc(t),n)}function Br(n,t,r){"__proto__"==t&&Tl?Tl(n,t,{configurable:!0,enumerable:!0,value:r,writable:!0}):n[t]=r}function Tr(n,t){for(var r=-1,e=t.length,u=il(e),i=null==n;++r<e;)u[r]=i?X:Mc(n,t[r]);return u}function Mr(n,t,r){return n===n&&(r!==X&&(n=n<=r?n:r),
t!==X&&(n=n>=t?n:t)),n}function Fr(n,t,e,u,i,o){var f,c=t&an,a=t&ln,l=t&sn;if(e&&(f=i?e(n,u,i,o):e(n)),f!==X)return f;if(!fc(n))return n;var s=bh(n);if(s){if(f=zi(n),!c)return Tu(n,f)}else{var h=zs(n),p=h==Kn||h==Vn;if(mh(n))return Iu(n,c);if(h==Yn||h==Dn||p&&!i){if(f=a||p?{}:Ei(n),!c)return a?Mu(n,Ur(f,n)):Du(n,Cr(f,n))}else{if(!Vr[h])return i?n:{};f=Si(n,h,c)}}o||(o=new wr);var _=o.get(n);if(_)return _;o.set(n,f),kh(n)?n.forEach(function(r){f.add(Fr(r,t,e,r,n,o))}):jh(n)&&n.forEach(function(r,u){
f.set(u,Fr(r,t,e,u,n,o))});var v=l?a?di:yi:a?qc:Pc,g=s?X:v(n);return r(g||n,function(r,u){g&&(u=r,r=n[u]),Sr(f,u,Fr(r,t,e,u,n,o))}),f}function Nr(n){var t=Pc(n);return function(r){return Pr(r,n,t)}}function Pr(n,t,r){var e=r.length;if(null==n)return!e;for(n=ll(n);e--;){var u=r[e],i=t[u],o=n[u];if(o===X&&!(u in n)||!i(o))return!1}return!0}function Gr(n,t,r){if("function"!=typeof n)throw new pl(en);return Ws(function(){n.apply(X,r)},t)}function Hr(n,t,r,e){var u=-1,i=o,a=!0,l=n.length,s=[],h=t.length;
if(!l)return s;r&&(t=c(t,z(r))),e?(i=f,a=!1):t.length>=tn&&(i=S,a=!1,t=new yr(t));n:for(;++u<l;){var p=n[u],_=null==r?p:r(p);if(p=e||0!==p?p:0,a&&_===_){for(var v=h;v--;)if(t[v]===_)continue n;s.push(p)}else i(t,_,e)||s.push(p)}return s}function Jr(n,t){var r=!0;return ys(n,function(n,e,u){return r=!!t(n,e,u)}),r}function Yr(n,t,r){for(var e=-1,u=n.length;++e<u;){var i=n[e],o=t(i);if(null!=o&&(f===X?o===o&&!bc(o):r(o,f)))var f=o,c=i}return c}function ne(n,t,r,e){var u=n.length;for(r=kc(r),r<0&&(r=-r>u?0:u+r),
e=e===X||e>u?u:kc(e),e<0&&(e+=u),e=r>e?0:Oc(e);r<e;)n[r++]=t;return n}function te(n,t){var r=[];return ys(n,function(n,e,u){t(n,e,u)&&r.push(n)}),r}function ee(n,t,r,e,u){var i=-1,o=n.length;for(r||(r=Li),u||(u=[]);++i<o;){var f=n[i];t>0&&r(f)?t>1?ee(f,t-1,r,e,u):a(u,f):e||(u[u.length]=f)}return u}function ue(n,t){return n&&bs(n,t,Pc)}function oe(n,t){return n&&ws(n,t,Pc)}function fe(n,t){return i(t,function(t){return uc(n[t])})}function _e(n,t){t=ku(t,n);for(var r=0,e=t.length;null!=n&&r<e;)n=n[no(t[r++])];
return r&&r==e?n:X}function de(n,t,r){var e=t(n);return bh(n)?e:a(e,r(n))}function we(n){return null==n?n===X?ut:Jn:Bl&&Bl in ll(n)?ki(n):Ki(n)}function me(n,t){return n>t}function xe(n,t){return null!=n&&bl.call(n,t)}function je(n,t){return null!=n&&t in ll(n)}function Ae(n,t,r){return n>=Hl(t,r)&&n<Gl(t,r)}function ke(n,t,r){for(var e=r?f:o,u=n[0].length,i=n.length,a=i,l=il(i),s=1/0,h=[];a--;){var p=n[a];a&&t&&(p=c(p,z(t))),s=Hl(p.length,s),l[a]=!r&&(t||u>=120&&p.length>=120)?new yr(a&&p):X}p=n[0];
var _=-1,v=l[0];n:for(;++_<u&&h.length<s;){var g=p[_],y=t?t(g):g;if(g=r||0!==g?g:0,!(v?S(v,y):e(h,y,r))){for(a=i;--a;){var d=l[a];if(!(d?S(d,y):e(n[a],y,r)))continue n}v&&v.push(y),h.push(g)}}return h}function Oe(n,t,r,e){return ue(n,function(n,u,i){t(e,r(n),u,i)}),e}function Ie(t,r,e){r=ku(r,t),t=Gi(t,r);var u=null==t?t:t[no(jo(r))];return null==u?X:n(u,t,e)}function Re(n){return cc(n)&&we(n)==Dn}function ze(n){return cc(n)&&we(n)==ft}function Ee(n){return cc(n)&&we(n)==Pn}function Se(n,t,r,e,u){
return n===t||(null==n||null==t||!cc(n)&&!cc(t)?n!==n&&t!==t:We(n,t,r,e,Se,u))}function We(n,t,r,e,u,i){var o=bh(n),f=bh(t),c=o?Mn:zs(n),a=f?Mn:zs(t);c=c==Dn?Yn:c,a=a==Dn?Yn:a;var l=c==Yn,s=a==Yn,h=c==a;if(h&&mh(n)){if(!mh(t))return!1;o=!0,l=!1}if(h&&!l)return i||(i=new wr),o||Oh(n)?pi(n,t,r,e,u,i):_i(n,t,c,r,e,u,i);if(!(r&hn)){var p=l&&bl.call(n,"__wrapped__"),_=s&&bl.call(t,"__wrapped__");if(p||_){var v=p?n.value():n,g=_?t.value():t;return i||(i=new wr),u(v,g,r,e,i)}}return!!h&&(i||(i=new wr),vi(n,t,r,e,u,i));
}function Le(n){return cc(n)&&zs(n)==Gn}function Ce(n,t,r,e){var u=r.length,i=u,o=!e;if(null==n)return!i;for(n=ll(n);u--;){var f=r[u];if(o&&f[2]?f[1]!==n[f[0]]:!(f[0]in n))return!1}for(;++u<i;){f=r[u];var c=f[0],a=n[c],l=f[1];if(o&&f[2]){if(a===X&&!(c in n))return!1}else{var s=new wr;if(e)var h=e(a,l,c,n,t,s);if(!(h===X?Se(l,a,hn|pn,e,s):h))return!1}}return!0}function Ue(n){return!(!fc(n)||Di(n))&&(uc(n)?kl:Zt).test(to(n))}function Be(n){return cc(n)&&we(n)==nt}function Te(n){return cc(n)&&zs(n)==tt;
}function $e(n){return cc(n)&&oc(n.length)&&!!Kr[we(n)]}function De(n){return"function"==typeof n?n:null==n?La:"object"==typeof n?bh(n)?Ze(n[0],n[1]):qe(n):Fa(n)}function Me(n){if(!Mi(n))return Vl(n);var t=[];for(var r in ll(n))bl.call(n,r)&&"constructor"!=r&&t.push(r);return t}function Fe(n){if(!fc(n))return Zi(n);var t=Mi(n),r=[];for(var e in n)("constructor"!=e||!t&&bl.call(n,e))&&r.push(e);return r}function Ne(n,t){return n<t}function Pe(n,t){var r=-1,e=Hf(n)?il(n.length):[];return ys(n,function(n,u,i){
e[++r]=t(n,u,i)}),e}function qe(n){var t=ji(n);return 1==t.length&&t[0][2]?Ni(t[0][0],t[0][1]):function(r){return r===n||Ce(r,n,t)}}function Ze(n,t){return Bi(n)&&Fi(t)?Ni(no(n),t):function(r){var e=Mc(r,n);return e===X&&e===t?Nc(r,n):Se(t,e,hn|pn)}}function Ke(n,t,r,e,u){n!==t&&bs(t,function(i,o){if(u||(u=new wr),fc(i))Ve(n,t,o,r,Ke,e,u);else{var f=e?e(Ji(n,o),i,o+"",n,t,u):X;f===X&&(f=i),Er(n,o,f)}},qc)}function Ve(n,t,r,e,u,i,o){var f=Ji(n,r),c=Ji(t,r),a=o.get(c);if(a)return Er(n,r,a),X;var l=i?i(f,c,r+"",n,t,o):X,s=l===X;
if(s){var h=bh(c),p=!h&&mh(c),_=!h&&!p&&Oh(c);l=c,h||p||_?bh(f)?l=f:Jf(f)?l=Tu(f):p?(s=!1,l=Iu(c,!0)):_?(s=!1,l=Wu(c,!0)):l=[]:gc(c)||dh(c)?(l=f,dh(f)?l=Rc(f):fc(f)&&!uc(f)||(l=Ei(c))):s=!1}s&&(o.set(c,l),u(l,c,e,i,o),o.delete(c)),Er(n,r,l)}function Ge(n,t){var r=n.length;if(r)return t+=t<0?r:0,Ci(t,r)?n[t]:X}function He(n,t,r){t=t.length?c(t,function(n){return bh(n)?function(t){return _e(t,1===n.length?n[0]:n)}:n}):[La];var e=-1;return t=c(t,z(mi())),A(Pe(n,function(n,r,u){return{criteria:c(t,function(t){
return t(n)}),index:++e,value:n}}),function(n,t){return Cu(n,t,r)})}function Je(n,t){return Ye(n,t,function(t,r){return Nc(n,r)})}function Ye(n,t,r){for(var e=-1,u=t.length,i={};++e<u;){var o=t[e],f=_e(n,o);r(f,o)&&fu(i,ku(o,n),f)}return i}function Qe(n){return function(t){return _e(t,n)}}function Xe(n,t,r,e){var u=e?d:y,i=-1,o=t.length,f=n;for(n===t&&(t=Tu(t)),r&&(f=c(n,z(r)));++i<o;)for(var a=0,l=t[i],s=r?r(l):l;(a=u(f,s,a,e))>-1;)f!==n&&Ll.call(f,a,1),Ll.call(n,a,1);return n}function nu(n,t){for(var r=n?t.length:0,e=r-1;r--;){
var u=t[r];if(r==e||u!==i){var i=u;Ci(u)?Ll.call(n,u,1):yu(n,u)}}return n}function tu(n,t){return n+Nl(Ql()*(t-n+1))}function ru(n,t,r,e){for(var u=-1,i=Gl(Fl((t-n)/(r||1)),0),o=il(i);i--;)o[e?i:++u]=n,n+=r;return o}function eu(n,t){var r="";if(!n||t<1||t>Wn)return r;do t%2&&(r+=n),t=Nl(t/2),t&&(n+=n);while(t);return r}function uu(n,t){return Ls(Vi(n,t,La),n+"")}function iu(n){return Ir(ra(n))}function ou(n,t){var r=ra(n);return Xi(r,Mr(t,0,r.length))}function fu(n,t,r,e){if(!fc(n))return n;t=ku(t,n);
for(var u=-1,i=t.length,o=i-1,f=n;null!=f&&++u<i;){var c=no(t[u]),a=r;if("__proto__"===c||"constructor"===c||"prototype"===c)return n;if(u!=o){var l=f[c];a=e?e(l,c,f):X,a===X&&(a=fc(l)?l:Ci(t[u+1])?[]:{})}Sr(f,c,a),f=f[c]}return n}function cu(n){return Xi(ra(n))}function au(n,t,r){var e=-1,u=n.length;t<0&&(t=-t>u?0:u+t),r=r>u?u:r,r<0&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0;for(var i=il(u);++e<u;)i[e]=n[e+t];return i}function lu(n,t){var r;return ys(n,function(n,e,u){return r=t(n,e,u),!r}),!!r}function su(n,t,r){
var e=0,u=null==n?e:n.length;if("number"==typeof t&&t===t&&u<=Tn){for(;e<u;){var i=e+u>>>1,o=n[i];null!==o&&!bc(o)&&(r?o<=t:o<t)?e=i+1:u=i}return u}return hu(n,t,La,r)}function hu(n,t,r,e){var u=0,i=null==n?0:n.length;if(0===i)return 0;t=r(t);for(var o=t!==t,f=null===t,c=bc(t),a=t===X;u<i;){var l=Nl((u+i)/2),s=r(n[l]),h=s!==X,p=null===s,_=s===s,v=bc(s);if(o)var g=e||_;else g=a?_&&(e||h):f?_&&h&&(e||!p):c?_&&h&&!p&&(e||!v):!p&&!v&&(e?s<=t:s<t);g?u=l+1:i=l}return Hl(i,Bn)}function pu(n,t){for(var r=-1,e=n.length,u=0,i=[];++r<e;){
var o=n[r],f=t?t(o):o;if(!r||!Gf(f,c)){var c=f;i[u++]=0===o?0:o}}return i}function _u(n){return"number"==typeof n?n:bc(n)?Cn:+n}function vu(n){if("string"==typeof n)return n;if(bh(n))return c(n,vu)+"";if(bc(n))return vs?vs.call(n):"";var t=n+"";return"0"==t&&1/n==-Sn?"-0":t}function gu(n,t,r){var e=-1,u=o,i=n.length,c=!0,a=[],l=a;if(r)c=!1,u=f;else if(i>=tn){var s=t?null:ks(n);if(s)return P(s);c=!1,u=S,l=new yr}else l=t?[]:a;n:for(;++e<i;){var h=n[e],p=t?t(h):h;if(h=r||0!==h?h:0,c&&p===p){for(var _=l.length;_--;)if(l[_]===p)continue n;
t&&l.push(p),a.push(h)}else u(l,p,r)||(l!==a&&l.push(p),a.push(h))}return a}function yu(n,t){return t=ku(t,n),n=Gi(n,t),null==n||delete n[no(jo(t))]}function du(n,t,r,e){return fu(n,t,r(_e(n,t)),e)}function bu(n,t,r,e){for(var u=n.length,i=e?u:-1;(e?i--:++i<u)&&t(n[i],i,n););return r?au(n,e?0:i,e?i+1:u):au(n,e?i+1:0,e?u:i)}function wu(n,t){var r=n;return r instanceof Ct&&(r=r.value()),l(t,function(n,t){return t.func.apply(t.thisArg,a([n],t.args))},r)}function mu(n,t,r){var e=n.length;if(e<2)return e?gu(n[0]):[];
for(var u=-1,i=il(e);++u<e;)for(var o=n[u],f=-1;++f<e;)f!=u&&(i[u]=Hr(i[u]||o,n[f],t,r));return gu(ee(i,1),t,r)}function xu(n,t,r){for(var e=-1,u=n.length,i=t.length,o={};++e<u;){r(o,n[e],e<i?t[e]:X)}return o}function ju(n){return Jf(n)?n:[]}function Au(n){return"function"==typeof n?n:La}function ku(n,t){return bh(n)?n:Bi(n,t)?[n]:Cs(Ec(n))}function Ou(n,t,r){var e=n.length;return r=r===X?e:r,!t&&r>=e?n:au(n,t,r)}function Iu(n,t){if(t)return n.slice();var r=n.length,e=zl?zl(r):new n.constructor(r);
return n.copy(e),e}function Ru(n){var t=new n.constructor(n.byteLength);return new Rl(t).set(new Rl(n)),t}function zu(n,t){return new n.constructor(t?Ru(n.buffer):n.buffer,n.byteOffset,n.byteLength)}function Eu(n){var t=new n.constructor(n.source,Nt.exec(n));return t.lastIndex=n.lastIndex,t}function Su(n){return _s?ll(_s.call(n)):{}}function Wu(n,t){return new n.constructor(t?Ru(n.buffer):n.buffer,n.byteOffset,n.length)}function Lu(n,t){if(n!==t){var r=n!==X,e=null===n,u=n===n,i=bc(n),o=t!==X,f=null===t,c=t===t,a=bc(t);
if(!f&&!a&&!i&&n>t||i&&o&&c&&!f&&!a||e&&o&&c||!r&&c||!u)return 1;if(!e&&!i&&!a&&n<t||a&&r&&u&&!e&&!i||f&&r&&u||!o&&u||!c)return-1}return 0}function Cu(n,t,r){for(var e=-1,u=n.criteria,i=t.criteria,o=u.length,f=r.length;++e<o;){var c=Lu(u[e],i[e]);if(c){if(e>=f)return c;return c*("desc"==r[e]?-1:1)}}return n.index-t.index}function Uu(n,t,r,e){for(var u=-1,i=n.length,o=r.length,f=-1,c=t.length,a=Gl(i-o,0),l=il(c+a),s=!e;++f<c;)l[f]=t[f];for(;++u<o;)(s||u<i)&&(l[r[u]]=n[u]);for(;a--;)l[f++]=n[u++];return l;
}function Bu(n,t,r,e){for(var u=-1,i=n.length,o=-1,f=r.length,c=-1,a=t.length,l=Gl(i-f,0),s=il(l+a),h=!e;++u<l;)s[u]=n[u];for(var p=u;++c<a;)s[p+c]=t[c];for(;++o<f;)(h||u<i)&&(s[p+r[o]]=n[u++]);return s}function Tu(n,t){var r=-1,e=n.length;for(t||(t=il(e));++r<e;)t[r]=n[r];return t}function $u(n,t,r,e){var u=!r;r||(r={});for(var i=-1,o=t.length;++i<o;){var f=t[i],c=e?e(r[f],n[f],f,r,n):X;c===X&&(c=n[f]),u?Br(r,f,c):Sr(r,f,c)}return r}function Du(n,t){return $u(n,Is(n),t)}function Mu(n,t){return $u(n,Rs(n),t);
}function Fu(n,r){return function(e,u){var i=bh(e)?t:Lr,o=r?r():{};return i(e,n,mi(u,2),o)}}function Nu(n){return uu(function(t,r){var e=-1,u=r.length,i=u>1?r[u-1]:X,o=u>2?r[2]:X;for(i=n.length>3&&"function"==typeof i?(u--,i):X,o&&Ui(r[0],r[1],o)&&(i=u<3?X:i,u=1),t=ll(t);++e<u;){var f=r[e];f&&n(t,f,e,i)}return t})}function Pu(n,t){return function(r,e){if(null==r)return r;if(!Hf(r))return n(r,e);for(var u=r.length,i=t?u:-1,o=ll(r);(t?i--:++i<u)&&e(o[i],i,o)!==!1;);return r}}function qu(n){return function(t,r,e){
for(var u=-1,i=ll(t),o=e(t),f=o.length;f--;){var c=o[n?f:++u];if(r(i[c],c,i)===!1)break}return t}}function Zu(n,t,r){function e(){return(this&&this!==re&&this instanceof e?i:n).apply(u?r:this,arguments)}var u=t&_n,i=Gu(n);return e}function Ku(n){return function(t){t=Ec(t);var r=T(t)?G(t):X,e=r?r[0]:t.charAt(0),u=r?Ou(r,1).join(""):t.slice(1);return e[n]()+u}}function Vu(n){return function(t){return l(Ra(ca(t).replace($r,"")),n,"")}}function Gu(n){return function(){var t=arguments;switch(t.length){
case 0:return new n;case 1:return new n(t[0]);case 2:return new n(t[0],t[1]);case 3:return new n(t[0],t[1],t[2]);case 4:return new n(t[0],t[1],t[2],t[3]);case 5:return new n(t[0],t[1],t[2],t[3],t[4]);case 6:return new n(t[0],t[1],t[2],t[3],t[4],t[5]);case 7:return new n(t[0],t[1],t[2],t[3],t[4],t[5],t[6])}var r=gs(n.prototype),e=n.apply(r,t);return fc(e)?e:r}}function Hu(t,r,e){function u(){for(var o=arguments.length,f=il(o),c=o,a=wi(u);c--;)f[c]=arguments[c];var l=o<3&&f[0]!==a&&f[o-1]!==a?[]:N(f,a);
return o-=l.length,o<e?oi(t,r,Qu,u.placeholder,X,f,l,X,X,e-o):n(this&&this!==re&&this instanceof u?i:t,this,f)}var i=Gu(t);return u}function Ju(n){return function(t,r,e){var u=ll(t);if(!Hf(t)){var i=mi(r,3);t=Pc(t),r=function(n){return i(u[n],n,u)}}var o=n(t,r,e);return o>-1?u[i?t[o]:o]:X}}function Yu(n){return gi(function(t){var r=t.length,e=r,u=Y.prototype.thru;for(n&&t.reverse();e--;){var i=t[e];if("function"!=typeof i)throw new pl(en);if(u&&!o&&"wrapper"==bi(i))var o=new Y([],!0)}for(e=o?e:r;++e<r;){
i=t[e];var f=bi(i),c="wrapper"==f?Os(i):X;o=c&&$i(c[0])&&c[1]==(mn|yn|bn|xn)&&!c[4].length&&1==c[9]?o[bi(c[0])].apply(o,c[3]):1==i.length&&$i(i)?o[f]():o.thru(i)}return function(){var n=arguments,e=n[0];if(o&&1==n.length&&bh(e))return o.plant(e).value();for(var u=0,i=r?t[u].apply(this,n):e;++u<r;)i=t[u].call(this,i);return i}})}function Qu(n,t,r,e,u,i,o,f,c,a){function l(){for(var y=arguments.length,d=il(y),b=y;b--;)d[b]=arguments[b];if(_)var w=wi(l),m=C(d,w);if(e&&(d=Uu(d,e,u,_)),i&&(d=Bu(d,i,o,_)),
y-=m,_&&y<a){return oi(n,t,Qu,l.placeholder,r,d,N(d,w),f,c,a-y)}var x=h?r:this,j=p?x[n]:n;return y=d.length,f?d=Hi(d,f):v&&y>1&&d.reverse(),s&&c<y&&(d.length=c),this&&this!==re&&this instanceof l&&(j=g||Gu(j)),j.apply(x,d)}var s=t&mn,h=t&_n,p=t&vn,_=t&(yn|dn),v=t&jn,g=p?X:Gu(n);return l}function Xu(n,t){return function(r,e){return Oe(r,n,t(e),{})}}function ni(n,t){return function(r,e){var u;if(r===X&&e===X)return t;if(r!==X&&(u=r),e!==X){if(u===X)return e;"string"==typeof r||"string"==typeof e?(r=vu(r),
e=vu(e)):(r=_u(r),e=_u(e)),u=n(r,e)}return u}}function ti(t){return gi(function(r){return r=c(r,z(mi())),uu(function(e){var u=this;return t(r,function(t){return n(t,u,e)})})})}function ri(n,t){t=t===X?" ":vu(t);var r=t.length;if(r<2)return r?eu(t,n):t;var e=eu(t,Fl(n/V(t)));return T(t)?Ou(G(e),0,n).join(""):e.slice(0,n)}function ei(t,r,e,u){function i(){for(var r=-1,c=arguments.length,a=-1,l=u.length,s=il(l+c),h=this&&this!==re&&this instanceof i?f:t;++a<l;)s[a]=u[a];for(;c--;)s[a++]=arguments[++r];
return n(h,o?e:this,s)}var o=r&_n,f=Gu(t);return i}function ui(n){return function(t,r,e){return e&&"number"!=typeof e&&Ui(t,r,e)&&(r=e=X),t=Ac(t),r===X?(r=t,t=0):r=Ac(r),e=e===X?t<r?1:-1:Ac(e),ru(t,r,e,n)}}function ii(n){return function(t,r){return"string"==typeof t&&"string"==typeof r||(t=Ic(t),r=Ic(r)),n(t,r)}}function oi(n,t,r,e,u,i,o,f,c,a){var l=t&yn,s=l?o:X,h=l?X:o,p=l?i:X,_=l?X:i;t|=l?bn:wn,t&=~(l?wn:bn),t&gn||(t&=~(_n|vn));var v=[n,t,u,p,s,_,h,f,c,a],g=r.apply(X,v);return $i(n)&&Ss(g,v),g.placeholder=e,
Yi(g,n,t)}function fi(n){var t=al[n];return function(n,r){if(n=Ic(n),r=null==r?0:Hl(kc(r),292),r&&Zl(n)){var e=(Ec(n)+"e").split("e");return e=(Ec(t(e[0]+"e"+(+e[1]+r)))+"e").split("e"),+(e[0]+"e"+(+e[1]-r))}return t(n)}}function ci(n){return function(t){var r=zs(t);return r==Gn?M(t):r==tt?q(t):I(t,n(t))}}function ai(n,t,r,e,u,i,o,f){var c=t&vn;if(!c&&"function"!=typeof n)throw new pl(en);var a=e?e.length:0;if(a||(t&=~(bn|wn),e=u=X),o=o===X?o:Gl(kc(o),0),f=f===X?f:kc(f),a-=u?u.length:0,t&wn){var l=e,s=u;
e=u=X}var h=c?X:Os(n),p=[n,t,r,e,u,l,s,i,o,f];if(h&&qi(p,h),n=p[0],t=p[1],r=p[2],e=p[3],u=p[4],f=p[9]=p[9]===X?c?0:n.length:Gl(p[9]-a,0),!f&&t&(yn|dn)&&(t&=~(yn|dn)),t&&t!=_n)_=t==yn||t==dn?Hu(n,t,f):t!=bn&&t!=(_n|bn)||u.length?Qu.apply(X,p):ei(n,t,r,e);else var _=Zu(n,t,r);return Yi((h?ms:Ss)(_,p),n,t)}function li(n,t,r,e){return n===X||Gf(n,gl[r])&&!bl.call(e,r)?t:n}function si(n,t,r,e,u,i){return fc(n)&&fc(t)&&(i.set(t,n),Ke(n,t,X,si,i),i.delete(t)),n}function hi(n){return gc(n)?X:n}function pi(n,t,r,e,u,i){
var o=r&hn,f=n.length,c=t.length;if(f!=c&&!(o&&c>f))return!1;var a=i.get(n),l=i.get(t);if(a&&l)return a==t&&l==n;var s=-1,p=!0,_=r&pn?new yr:X;for(i.set(n,t),i.set(t,n);++s<f;){var v=n[s],g=t[s];if(e)var y=o?e(g,v,s,t,n,i):e(v,g,s,n,t,i);if(y!==X){if(y)continue;p=!1;break}if(_){if(!h(t,function(n,t){if(!S(_,t)&&(v===n||u(v,n,r,e,i)))return _.push(t)})){p=!1;break}}else if(v!==g&&!u(v,g,r,e,i)){p=!1;break}}return i.delete(n),i.delete(t),p}function _i(n,t,r,e,u,i,o){switch(r){case ct:if(n.byteLength!=t.byteLength||n.byteOffset!=t.byteOffset)return!1;
n=n.buffer,t=t.buffer;case ft:return!(n.byteLength!=t.byteLength||!i(new Rl(n),new Rl(t)));case Nn:case Pn:case Hn:return Gf(+n,+t);case Zn:return n.name==t.name&&n.message==t.message;case nt:case rt:return n==t+"";case Gn:var f=M;case tt:var c=e&hn;if(f||(f=P),n.size!=t.size&&!c)return!1;var a=o.get(n);if(a)return a==t;e|=pn,o.set(n,t);var l=pi(f(n),f(t),e,u,i,o);return o.delete(n),l;case et:if(_s)return _s.call(n)==_s.call(t)}return!1}function vi(n,t,r,e,u,i){var o=r&hn,f=yi(n),c=f.length;if(c!=yi(t).length&&!o)return!1;
for(var a=c;a--;){var l=f[a];if(!(o?l in t:bl.call(t,l)))return!1}var s=i.get(n),h=i.get(t);if(s&&h)return s==t&&h==n;var p=!0;i.set(n,t),i.set(t,n);for(var _=o;++a<c;){l=f[a];var v=n[l],g=t[l];if(e)var y=o?e(g,v,l,t,n,i):e(v,g,l,n,t,i);if(!(y===X?v===g||u(v,g,r,e,i):y)){p=!1;break}_||(_="constructor"==l)}if(p&&!_){var d=n.constructor,b=t.constructor;d!=b&&"constructor"in n&&"constructor"in t&&!("function"==typeof d&&d instanceof d&&"function"==typeof b&&b instanceof b)&&(p=!1)}return i.delete(n),
i.delete(t),p}function gi(n){return Ls(Vi(n,X,_o),n+"")}function yi(n){return de(n,Pc,Is)}function di(n){return de(n,qc,Rs)}function bi(n){for(var t=n.name+"",r=fs[t],e=bl.call(fs,t)?r.length:0;e--;){var u=r[e],i=u.func;if(null==i||i==n)return u.name}return t}function wi(n){return(bl.call(Z,"placeholder")?Z:n).placeholder}function mi(){var n=Z.iteratee||Ca;return n=n===Ca?De:n,arguments.length?n(arguments[0],arguments[1]):n}function xi(n,t){var r=n.__data__;return Ti(t)?r["string"==typeof t?"string":"hash"]:r.map;
}function ji(n){for(var t=Pc(n),r=t.length;r--;){var e=t[r],u=n[e];t[r]=[e,u,Fi(u)]}return t}function Ai(n,t){var r=B(n,t);return Ue(r)?r:X}function ki(n){var t=bl.call(n,Bl),r=n[Bl];try{n[Bl]=X;var e=!0}catch(n){}var u=xl.call(n);return e&&(t?n[Bl]=r:delete n[Bl]),u}function Oi(n,t,r){for(var e=-1,u=r.length;++e<u;){var i=r[e],o=i.size;switch(i.type){case"drop":n+=o;break;case"dropRight":t-=o;break;case"take":t=Hl(t,n+o);break;case"takeRight":n=Gl(n,t-o)}}return{start:n,end:t}}function Ii(n){var t=n.match(Bt);
return t?t[1].split(Tt):[]}function Ri(n,t,r){t=ku(t,n);for(var e=-1,u=t.length,i=!1;++e<u;){var o=no(t[e]);if(!(i=null!=n&&r(n,o)))break;n=n[o]}return i||++e!=u?i:(u=null==n?0:n.length,!!u&&oc(u)&&Ci(o,u)&&(bh(n)||dh(n)))}function zi(n){var t=n.length,r=new n.constructor(t);return t&&"string"==typeof n[0]&&bl.call(n,"index")&&(r.index=n.index,r.input=n.input),r}function Ei(n){return"function"!=typeof n.constructor||Mi(n)?{}:gs(El(n))}function Si(n,t,r){var e=n.constructor;switch(t){case ft:return Ru(n);
case Nn:case Pn:return new e(+n);case ct:return zu(n,r);case at:case lt:case st:case ht:case pt:case _t:case vt:case gt:case yt:return Wu(n,r);case Gn:return new e;case Hn:case rt:return new e(n);case nt:return Eu(n);case tt:return new e;case et:return Su(n)}}function Wi(n,t){var r=t.length;if(!r)return n;var e=r-1;return t[e]=(r>1?"& ":"")+t[e],t=t.join(r>2?", ":" "),n.replace(Ut,"{\n/* [wrapped with "+t+"] */\n")}function Li(n){return bh(n)||dh(n)||!!(Cl&&n&&n[Cl])}function Ci(n,t){var r=typeof n;
return t=null==t?Wn:t,!!t&&("number"==r||"symbol"!=r&&Vt.test(n))&&n>-1&&n%1==0&&n<t}function Ui(n,t,r){if(!fc(r))return!1;var e=typeof t;return!!("number"==e?Hf(r)&&Ci(t,r.length):"string"==e&&t in r)&&Gf(r[t],n)}function Bi(n,t){if(bh(n))return!1;var r=typeof n;return!("number"!=r&&"symbol"!=r&&"boolean"!=r&&null!=n&&!bc(n))||(zt.test(n)||!Rt.test(n)||null!=t&&n in ll(t))}function Ti(n){var t=typeof n;return"string"==t||"number"==t||"symbol"==t||"boolean"==t?"__proto__"!==n:null===n}function $i(n){
var t=bi(n),r=Z[t];if("function"!=typeof r||!(t in Ct.prototype))return!1;if(n===r)return!0;var e=Os(r);return!!e&&n===e[0]}function Di(n){return!!ml&&ml in n}function Mi(n){var t=n&&n.constructor;return n===("function"==typeof t&&t.prototype||gl)}function Fi(n){return n===n&&!fc(n)}function Ni(n,t){return function(r){return null!=r&&(r[n]===t&&(t!==X||n in ll(r)))}}function Pi(n){var t=Cf(n,function(n){return r.size===fn&&r.clear(),n}),r=t.cache;return t}function qi(n,t){var r=n[1],e=t[1],u=r|e,i=u<(_n|vn|mn),o=e==mn&&r==yn||e==mn&&r==xn&&n[7].length<=t[8]||e==(mn|xn)&&t[7].length<=t[8]&&r==yn;
if(!i&&!o)return n;e&_n&&(n[2]=t[2],u|=r&_n?0:gn);var f=t[3];if(f){var c=n[3];n[3]=c?Uu(c,f,t[4]):f,n[4]=c?N(n[3],cn):t[4]}return f=t[5],f&&(c=n[5],n[5]=c?Bu(c,f,t[6]):f,n[6]=c?N(n[5],cn):t[6]),f=t[7],f&&(n[7]=f),e&mn&&(n[8]=null==n[8]?t[8]:Hl(n[8],t[8])),null==n[9]&&(n[9]=t[9]),n[0]=t[0],n[1]=u,n}function Zi(n){var t=[];if(null!=n)for(var r in ll(n))t.push(r);return t}function Ki(n){return xl.call(n)}function Vi(t,r,e){return r=Gl(r===X?t.length-1:r,0),function(){for(var u=arguments,i=-1,o=Gl(u.length-r,0),f=il(o);++i<o;)f[i]=u[r+i];
i=-1;for(var c=il(r+1);++i<r;)c[i]=u[i];return c[r]=e(f),n(t,this,c)}}function Gi(n,t){return t.length<2?n:_e(n,au(t,0,-1))}function Hi(n,t){for(var r=n.length,e=Hl(t.length,r),u=Tu(n);e--;){var i=t[e];n[e]=Ci(i,r)?u[i]:X}return n}function Ji(n,t){if(("constructor"!==t||"function"!=typeof n[t])&&"__proto__"!=t)return n[t]}function Yi(n,t,r){var e=t+"";return Ls(n,Wi(e,ro(Ii(e),r)))}function Qi(n){var t=0,r=0;return function(){var e=Jl(),u=In-(e-r);if(r=e,u>0){if(++t>=On)return arguments[0]}else t=0;
return n.apply(X,arguments)}}function Xi(n,t){var r=-1,e=n.length,u=e-1;for(t=t===X?e:t;++r<t;){var i=tu(r,u),o=n[i];n[i]=n[r],n[r]=o}return n.length=t,n}function no(n){if("string"==typeof n||bc(n))return n;var t=n+"";return"0"==t&&1/n==-Sn?"-0":t}function to(n){if(null!=n){try{return dl.call(n)}catch(n){}try{return n+""}catch(n){}}return""}function ro(n,t){return r($n,function(r){var e="_."+r[0];t&r[1]&&!o(n,e)&&n.push(e)}),n.sort()}function eo(n){if(n instanceof Ct)return n.clone();var t=new Y(n.__wrapped__,n.__chain__);
return t.__actions__=Tu(n.__actions__),t.__index__=n.__index__,t.__values__=n.__values__,t}function uo(n,t,r){t=(r?Ui(n,t,r):t===X)?1:Gl(kc(t),0);var e=null==n?0:n.length;if(!e||t<1)return[];for(var u=0,i=0,o=il(Fl(e/t));u<e;)o[i++]=au(n,u,u+=t);return o}function io(n){for(var t=-1,r=null==n?0:n.length,e=0,u=[];++t<r;){var i=n[t];i&&(u[e++]=i)}return u}function oo(){var n=arguments.length;if(!n)return[];for(var t=il(n-1),r=arguments[0],e=n;e--;)t[e-1]=arguments[e];return a(bh(r)?Tu(r):[r],ee(t,1));
}function fo(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===X?1:kc(t),au(n,t<0?0:t,e)):[]}function co(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===X?1:kc(t),t=e-t,au(n,0,t<0?0:t)):[]}function ao(n,t){return n&&n.length?bu(n,mi(t,3),!0,!0):[]}function lo(n,t){return n&&n.length?bu(n,mi(t,3),!0):[]}function so(n,t,r,e){var u=null==n?0:n.length;return u?(r&&"number"!=typeof r&&Ui(n,t,r)&&(r=0,e=u),ne(n,t,r,e)):[]}function ho(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=null==r?0:kc(r);
return u<0&&(u=Gl(e+u,0)),g(n,mi(t,3),u)}function po(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=e-1;return r!==X&&(u=kc(r),u=r<0?Gl(e+u,0):Hl(u,e-1)),g(n,mi(t,3),u,!0)}function _o(n){return(null==n?0:n.length)?ee(n,1):[]}function vo(n){return(null==n?0:n.length)?ee(n,Sn):[]}function go(n,t){return(null==n?0:n.length)?(t=t===X?1:kc(t),ee(n,t)):[]}function yo(n){for(var t=-1,r=null==n?0:n.length,e={};++t<r;){var u=n[t];e[u[0]]=u[1]}return e}function bo(n){return n&&n.length?n[0]:X}function wo(n,t,r){
var e=null==n?0:n.length;if(!e)return-1;var u=null==r?0:kc(r);return u<0&&(u=Gl(e+u,0)),y(n,t,u)}function mo(n){return(null==n?0:n.length)?au(n,0,-1):[]}function xo(n,t){return null==n?"":Kl.call(n,t)}function jo(n){var t=null==n?0:n.length;return t?n[t-1]:X}function Ao(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=e;return r!==X&&(u=kc(r),u=u<0?Gl(e+u,0):Hl(u,e-1)),t===t?K(n,t,u):g(n,b,u,!0)}function ko(n,t){return n&&n.length?Ge(n,kc(t)):X}function Oo(n,t){return n&&n.length&&t&&t.length?Xe(n,t):n;
}function Io(n,t,r){return n&&n.length&&t&&t.length?Xe(n,t,mi(r,2)):n}function Ro(n,t,r){return n&&n.length&&t&&t.length?Xe(n,t,X,r):n}function zo(n,t){var r=[];if(!n||!n.length)return r;var e=-1,u=[],i=n.length;for(t=mi(t,3);++e<i;){var o=n[e];t(o,e,n)&&(r.push(o),u.push(e))}return nu(n,u),r}function Eo(n){return null==n?n:Xl.call(n)}function So(n,t,r){var e=null==n?0:n.length;return e?(r&&"number"!=typeof r&&Ui(n,t,r)?(t=0,r=e):(t=null==t?0:kc(t),r=r===X?e:kc(r)),au(n,t,r)):[]}function Wo(n,t){
return su(n,t)}function Lo(n,t,r){return hu(n,t,mi(r,2))}function Co(n,t){var r=null==n?0:n.length;if(r){var e=su(n,t);if(e<r&&Gf(n[e],t))return e}return-1}function Uo(n,t){return su(n,t,!0)}function Bo(n,t,r){return hu(n,t,mi(r,2),!0)}function To(n,t){if(null==n?0:n.length){var r=su(n,t,!0)-1;if(Gf(n[r],t))return r}return-1}function $o(n){return n&&n.length?pu(n):[]}function Do(n,t){return n&&n.length?pu(n,mi(t,2)):[]}function Mo(n){var t=null==n?0:n.length;return t?au(n,1,t):[]}function Fo(n,t,r){
return n&&n.length?(t=r||t===X?1:kc(t),au(n,0,t<0?0:t)):[]}function No(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===X?1:kc(t),t=e-t,au(n,t<0?0:t,e)):[]}function Po(n,t){return n&&n.length?bu(n,mi(t,3),!1,!0):[]}function qo(n,t){return n&&n.length?bu(n,mi(t,3)):[]}function Zo(n){return n&&n.length?gu(n):[]}function Ko(n,t){return n&&n.length?gu(n,mi(t,2)):[]}function Vo(n,t){return t="function"==typeof t?t:X,n&&n.length?gu(n,X,t):[]}function Go(n){if(!n||!n.length)return[];var t=0;return n=i(n,function(n){
if(Jf(n))return t=Gl(n.length,t),!0}),O(t,function(t){return c(n,m(t))})}function Ho(t,r){if(!t||!t.length)return[];var e=Go(t);return null==r?e:c(e,function(t){return n(r,X,t)})}function Jo(n,t){return xu(n||[],t||[],Sr)}function Yo(n,t){return xu(n||[],t||[],fu)}function Qo(n){var t=Z(n);return t.__chain__=!0,t}function Xo(n,t){return t(n),n}function nf(n,t){return t(n)}function tf(){return Qo(this)}function rf(){return new Y(this.value(),this.__chain__)}function ef(){this.__values__===X&&(this.__values__=jc(this.value()));
var n=this.__index__>=this.__values__.length;return{done:n,value:n?X:this.__values__[this.__index__++]}}function uf(){return this}function of(n){for(var t,r=this;r instanceof J;){var e=eo(r);e.__index__=0,e.__values__=X,t?u.__wrapped__=e:t=e;var u=e;r=r.__wrapped__}return u.__wrapped__=n,t}function ff(){var n=this.__wrapped__;if(n instanceof Ct){var t=n;return this.__actions__.length&&(t=new Ct(this)),t=t.reverse(),t.__actions__.push({func:nf,args:[Eo],thisArg:X}),new Y(t,this.__chain__)}return this.thru(Eo);
}function cf(){return wu(this.__wrapped__,this.__actions__)}function af(n,t,r){var e=bh(n)?u:Jr;return r&&Ui(n,t,r)&&(t=X),e(n,mi(t,3))}function lf(n,t){return(bh(n)?i:te)(n,mi(t,3))}function sf(n,t){return ee(yf(n,t),1)}function hf(n,t){return ee(yf(n,t),Sn)}function pf(n,t,r){return r=r===X?1:kc(r),ee(yf(n,t),r)}function _f(n,t){return(bh(n)?r:ys)(n,mi(t,3))}function vf(n,t){return(bh(n)?e:ds)(n,mi(t,3))}function gf(n,t,r,e){n=Hf(n)?n:ra(n),r=r&&!e?kc(r):0;var u=n.length;return r<0&&(r=Gl(u+r,0)),
dc(n)?r<=u&&n.indexOf(t,r)>-1:!!u&&y(n,t,r)>-1}function yf(n,t){return(bh(n)?c:Pe)(n,mi(t,3))}function df(n,t,r,e){return null==n?[]:(bh(t)||(t=null==t?[]:[t]),r=e?X:r,bh(r)||(r=null==r?[]:[r]),He(n,t,r))}function bf(n,t,r){var e=bh(n)?l:j,u=arguments.length<3;return e(n,mi(t,4),r,u,ys)}function wf(n,t,r){var e=bh(n)?s:j,u=arguments.length<3;return e(n,mi(t,4),r,u,ds)}function mf(n,t){return(bh(n)?i:te)(n,Uf(mi(t,3)))}function xf(n){return(bh(n)?Ir:iu)(n)}function jf(n,t,r){return t=(r?Ui(n,t,r):t===X)?1:kc(t),
(bh(n)?Rr:ou)(n,t)}function Af(n){return(bh(n)?zr:cu)(n)}function kf(n){if(null==n)return 0;if(Hf(n))return dc(n)?V(n):n.length;var t=zs(n);return t==Gn||t==tt?n.size:Me(n).length}function Of(n,t,r){var e=bh(n)?h:lu;return r&&Ui(n,t,r)&&(t=X),e(n,mi(t,3))}function If(n,t){if("function"!=typeof t)throw new pl(en);return n=kc(n),function(){if(--n<1)return t.apply(this,arguments)}}function Rf(n,t,r){return t=r?X:t,t=n&&null==t?n.length:t,ai(n,mn,X,X,X,X,t)}function zf(n,t){var r;if("function"!=typeof t)throw new pl(en);
return n=kc(n),function(){return--n>0&&(r=t.apply(this,arguments)),n<=1&&(t=X),r}}function Ef(n,t,r){t=r?X:t;var e=ai(n,yn,X,X,X,X,X,t);return e.placeholder=Ef.placeholder,e}function Sf(n,t,r){t=r?X:t;var e=ai(n,dn,X,X,X,X,X,t);return e.placeholder=Sf.placeholder,e}function Wf(n,t,r){function e(t){var r=h,e=p;return h=p=X,d=t,v=n.apply(e,r)}function u(n){return d=n,g=Ws(f,t),b?e(n):v}function i(n){var r=n-y,e=n-d,u=t-r;return w?Hl(u,_-e):u}function o(n){var r=n-y,e=n-d;return y===X||r>=t||r<0||w&&e>=_;
}function f(){var n=fh();return o(n)?c(n):(g=Ws(f,i(n)),X)}function c(n){return g=X,m&&h?e(n):(h=p=X,v)}function a(){g!==X&&As(g),d=0,h=y=p=g=X}function l(){return g===X?v:c(fh())}function s(){var n=fh(),r=o(n);if(h=arguments,p=this,y=n,r){if(g===X)return u(y);if(w)return As(g),g=Ws(f,t),e(y)}return g===X&&(g=Ws(f,t)),v}var h,p,_,v,g,y,d=0,b=!1,w=!1,m=!0;if("function"!=typeof n)throw new pl(en);return t=Ic(t)||0,fc(r)&&(b=!!r.leading,w="maxWait"in r,_=w?Gl(Ic(r.maxWait)||0,t):_,m="trailing"in r?!!r.trailing:m),
s.cancel=a,s.flush=l,s}function Lf(n){return ai(n,jn)}function Cf(n,t){if("function"!=typeof n||null!=t&&"function"!=typeof t)throw new pl(en);var r=function(){var e=arguments,u=t?t.apply(this,e):e[0],i=r.cache;if(i.has(u))return i.get(u);var o=n.apply(this,e);return r.cache=i.set(u,o)||i,o};return r.cache=new(Cf.Cache||sr),r}function Uf(n){if("function"!=typeof n)throw new pl(en);return function(){var t=arguments;switch(t.length){case 0:return!n.call(this);case 1:return!n.call(this,t[0]);case 2:
return!n.call(this,t[0],t[1]);case 3:return!n.call(this,t[0],t[1],t[2])}return!n.apply(this,t)}}function Bf(n){return zf(2,n)}function Tf(n,t){if("function"!=typeof n)throw new pl(en);return t=t===X?t:kc(t),uu(n,t)}function $f(t,r){if("function"!=typeof t)throw new pl(en);return r=null==r?0:Gl(kc(r),0),uu(function(e){var u=e[r],i=Ou(e,0,r);return u&&a(i,u),n(t,this,i)})}function Df(n,t,r){var e=!0,u=!0;if("function"!=typeof n)throw new pl(en);return fc(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),
Wf(n,t,{leading:e,maxWait:t,trailing:u})}function Mf(n){return Rf(n,1)}function Ff(n,t){return ph(Au(t),n)}function Nf(){if(!arguments.length)return[];var n=arguments[0];return bh(n)?n:[n]}function Pf(n){return Fr(n,sn)}function qf(n,t){return t="function"==typeof t?t:X,Fr(n,sn,t)}function Zf(n){return Fr(n,an|sn)}function Kf(n,t){return t="function"==typeof t?t:X,Fr(n,an|sn,t)}function Vf(n,t){return null==t||Pr(n,t,Pc(t))}function Gf(n,t){return n===t||n!==n&&t!==t}function Hf(n){return null!=n&&oc(n.length)&&!uc(n);
}function Jf(n){return cc(n)&&Hf(n)}function Yf(n){return n===!0||n===!1||cc(n)&&we(n)==Nn}function Qf(n){return cc(n)&&1===n.nodeType&&!gc(n)}function Xf(n){if(null==n)return!0;if(Hf(n)&&(bh(n)||"string"==typeof n||"function"==typeof n.splice||mh(n)||Oh(n)||dh(n)))return!n.length;var t=zs(n);if(t==Gn||t==tt)return!n.size;if(Mi(n))return!Me(n).length;for(var r in n)if(bl.call(n,r))return!1;return!0}function nc(n,t){return Se(n,t)}function tc(n,t,r){r="function"==typeof r?r:X;var e=r?r(n,t):X;return e===X?Se(n,t,X,r):!!e;
}function rc(n){if(!cc(n))return!1;var t=we(n);return t==Zn||t==qn||"string"==typeof n.message&&"string"==typeof n.name&&!gc(n)}function ec(n){return"number"==typeof n&&Zl(n)}function uc(n){if(!fc(n))return!1;var t=we(n);return t==Kn||t==Vn||t==Fn||t==Xn}function ic(n){return"number"==typeof n&&n==kc(n)}function oc(n){return"number"==typeof n&&n>-1&&n%1==0&&n<=Wn}function fc(n){var t=typeof n;return null!=n&&("object"==t||"function"==t)}function cc(n){return null!=n&&"object"==typeof n}function ac(n,t){
return n===t||Ce(n,t,ji(t))}function lc(n,t,r){return r="function"==typeof r?r:X,Ce(n,t,ji(t),r)}function sc(n){return vc(n)&&n!=+n}function hc(n){if(Es(n))throw new fl(rn);return Ue(n)}function pc(n){return null===n}function _c(n){return null==n}function vc(n){return"number"==typeof n||cc(n)&&we(n)==Hn}function gc(n){if(!cc(n)||we(n)!=Yn)return!1;var t=El(n);if(null===t)return!0;var r=bl.call(t,"constructor")&&t.constructor;return"function"==typeof r&&r instanceof r&&dl.call(r)==jl}function yc(n){
return ic(n)&&n>=-Wn&&n<=Wn}function dc(n){return"string"==typeof n||!bh(n)&&cc(n)&&we(n)==rt}function bc(n){return"symbol"==typeof n||cc(n)&&we(n)==et}function wc(n){return n===X}function mc(n){return cc(n)&&zs(n)==it}function xc(n){return cc(n)&&we(n)==ot}function jc(n){if(!n)return[];if(Hf(n))return dc(n)?G(n):Tu(n);if(Ul&&n[Ul])return D(n[Ul]());var t=zs(n);return(t==Gn?M:t==tt?P:ra)(n)}function Ac(n){if(!n)return 0===n?n:0;if(n=Ic(n),n===Sn||n===-Sn){return(n<0?-1:1)*Ln}return n===n?n:0}function kc(n){
var t=Ac(n),r=t%1;return t===t?r?t-r:t:0}function Oc(n){return n?Mr(kc(n),0,Un):0}function Ic(n){if("number"==typeof n)return n;if(bc(n))return Cn;if(fc(n)){var t="function"==typeof n.valueOf?n.valueOf():n;n=fc(t)?t+"":t}if("string"!=typeof n)return 0===n?n:+n;n=R(n);var r=qt.test(n);return r||Kt.test(n)?Xr(n.slice(2),r?2:8):Pt.test(n)?Cn:+n}function Rc(n){return $u(n,qc(n))}function zc(n){return n?Mr(kc(n),-Wn,Wn):0===n?n:0}function Ec(n){return null==n?"":vu(n)}function Sc(n,t){var r=gs(n);return null==t?r:Cr(r,t);
}function Wc(n,t){return v(n,mi(t,3),ue)}function Lc(n,t){return v(n,mi(t,3),oe)}function Cc(n,t){return null==n?n:bs(n,mi(t,3),qc)}function Uc(n,t){return null==n?n:ws(n,mi(t,3),qc)}function Bc(n,t){return n&&ue(n,mi(t,3))}function Tc(n,t){return n&&oe(n,mi(t,3))}function $c(n){return null==n?[]:fe(n,Pc(n))}function Dc(n){return null==n?[]:fe(n,qc(n))}function Mc(n,t,r){var e=null==n?X:_e(n,t);return e===X?r:e}function Fc(n,t){return null!=n&&Ri(n,t,xe)}function Nc(n,t){return null!=n&&Ri(n,t,je);
}function Pc(n){return Hf(n)?Or(n):Me(n)}function qc(n){return Hf(n)?Or(n,!0):Fe(n)}function Zc(n,t){var r={};return t=mi(t,3),ue(n,function(n,e,u){Br(r,t(n,e,u),n)}),r}function Kc(n,t){var r={};return t=mi(t,3),ue(n,function(n,e,u){Br(r,e,t(n,e,u))}),r}function Vc(n,t){return Gc(n,Uf(mi(t)))}function Gc(n,t){if(null==n)return{};var r=c(di(n),function(n){return[n]});return t=mi(t),Ye(n,r,function(n,r){return t(n,r[0])})}function Hc(n,t,r){t=ku(t,n);var e=-1,u=t.length;for(u||(u=1,n=X);++e<u;){var i=null==n?X:n[no(t[e])];
i===X&&(e=u,i=r),n=uc(i)?i.call(n):i}return n}function Jc(n,t,r){return null==n?n:fu(n,t,r)}function Yc(n,t,r,e){return e="function"==typeof e?e:X,null==n?n:fu(n,t,r,e)}function Qc(n,t,e){var u=bh(n),i=u||mh(n)||Oh(n);if(t=mi(t,4),null==e){var o=n&&n.constructor;e=i?u?new o:[]:fc(n)&&uc(o)?gs(El(n)):{}}return(i?r:ue)(n,function(n,r,u){return t(e,n,r,u)}),e}function Xc(n,t){return null==n||yu(n,t)}function na(n,t,r){return null==n?n:du(n,t,Au(r))}function ta(n,t,r,e){return e="function"==typeof e?e:X,
null==n?n:du(n,t,Au(r),e)}function ra(n){return null==n?[]:E(n,Pc(n))}function ea(n){return null==n?[]:E(n,qc(n))}function ua(n,t,r){return r===X&&(r=t,t=X),r!==X&&(r=Ic(r),r=r===r?r:0),t!==X&&(t=Ic(t),t=t===t?t:0),Mr(Ic(n),t,r)}function ia(n,t,r){return t=Ac(t),r===X?(r=t,t=0):r=Ac(r),n=Ic(n),Ae(n,t,r)}function oa(n,t,r){if(r&&"boolean"!=typeof r&&Ui(n,t,r)&&(t=r=X),r===X&&("boolean"==typeof t?(r=t,t=X):"boolean"==typeof n&&(r=n,n=X)),n===X&&t===X?(n=0,t=1):(n=Ac(n),t===X?(t=n,n=0):t=Ac(t)),n>t){
var e=n;n=t,t=e}if(r||n%1||t%1){var u=Ql();return Hl(n+u*(t-n+Qr("1e-"+((u+"").length-1))),t)}return tu(n,t)}function fa(n){return Qh(Ec(n).toLowerCase())}function ca(n){return n=Ec(n),n&&n.replace(Gt,ve).replace(Dr,"")}function aa(n,t,r){n=Ec(n),t=vu(t);var e=n.length;r=r===X?e:Mr(kc(r),0,e);var u=r;return r-=t.length,r>=0&&n.slice(r,u)==t}function la(n){return n=Ec(n),n&&At.test(n)?n.replace(xt,ge):n}function sa(n){return n=Ec(n),n&&Wt.test(n)?n.replace(St,"\\$&"):n}function ha(n,t,r){n=Ec(n),t=kc(t);
var e=t?V(n):0;if(!t||e>=t)return n;var u=(t-e)/2;return ri(Nl(u),r)+n+ri(Fl(u),r)}function pa(n,t,r){n=Ec(n),t=kc(t);var e=t?V(n):0;return t&&e<t?n+ri(t-e,r):n}function _a(n,t,r){n=Ec(n),t=kc(t);var e=t?V(n):0;return t&&e<t?ri(t-e,r)+n:n}function va(n,t,r){return r||null==t?t=0:t&&(t=+t),Yl(Ec(n).replace(Lt,""),t||0)}function ga(n,t,r){return t=(r?Ui(n,t,r):t===X)?1:kc(t),eu(Ec(n),t)}function ya(){var n=arguments,t=Ec(n[0]);return n.length<3?t:t.replace(n[1],n[2])}function da(n,t,r){return r&&"number"!=typeof r&&Ui(n,t,r)&&(t=r=X),
(r=r===X?Un:r>>>0)?(n=Ec(n),n&&("string"==typeof t||null!=t&&!Ah(t))&&(t=vu(t),!t&&T(n))?Ou(G(n),0,r):n.split(t,r)):[]}function ba(n,t,r){return n=Ec(n),r=null==r?0:Mr(kc(r),0,n.length),t=vu(t),n.slice(r,r+t.length)==t}function wa(n,t,r){var e=Z.templateSettings;r&&Ui(n,t,r)&&(t=X),n=Ec(n),t=Sh({},t,e,li);var u,i,o=Sh({},t.imports,e.imports,li),f=Pc(o),c=E(o,f),a=0,l=t.interpolate||Ht,s="__p += '",h=sl((t.escape||Ht).source+"|"+l.source+"|"+(l===It?Ft:Ht).source+"|"+(t.evaluate||Ht).source+"|$","g"),p="//# sourceURL="+(bl.call(t,"sourceURL")?(t.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++Zr+"]")+"\n";
n.replace(h,function(t,r,e,o,f,c){return e||(e=o),s+=n.slice(a,c).replace(Jt,U),r&&(u=!0,s+="' +\n__e("+r+") +\n'"),f&&(i=!0,s+="';\n"+f+";\n__p += '"),e&&(s+="' +\n((__t = ("+e+")) == null ? '' : __t) +\n'"),a=c+t.length,t}),s+="';\n";var _=bl.call(t,"variable")&&t.variable;if(_){if(Dt.test(_))throw new fl(un)}else s="with (obj) {\n"+s+"\n}\n";s=(i?s.replace(dt,""):s).replace(bt,"$1").replace(wt,"$1;"),s="function("+(_||"obj")+") {\n"+(_?"":"obj || (obj = {});\n")+"var __t, __p = ''"+(u?", __e = _.escape":"")+(i?", __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, '') }\n":";\n")+s+"return __p\n}";
var v=Xh(function(){return cl(f,p+"return "+s).apply(X,c)});if(v.source=s,rc(v))throw v;return v}function ma(n){return Ec(n).toLowerCase()}function xa(n){return Ec(n).toUpperCase()}function ja(n,t,r){if(n=Ec(n),n&&(r||t===X))return R(n);if(!n||!(t=vu(t)))return n;var e=G(n),u=G(t);return Ou(e,W(e,u),L(e,u)+1).join("")}function Aa(n,t,r){if(n=Ec(n),n&&(r||t===X))return n.slice(0,H(n)+1);if(!n||!(t=vu(t)))return n;var e=G(n);return Ou(e,0,L(e,G(t))+1).join("")}function ka(n,t,r){if(n=Ec(n),n&&(r||t===X))return n.replace(Lt,"");
if(!n||!(t=vu(t)))return n;var e=G(n);return Ou(e,W(e,G(t))).join("")}function Oa(n,t){var r=An,e=kn;if(fc(t)){var u="separator"in t?t.separator:u;r="length"in t?kc(t.length):r,e="omission"in t?vu(t.omission):e}n=Ec(n);var i=n.length;if(T(n)){var o=G(n);i=o.length}if(r>=i)return n;var f=r-V(e);if(f<1)return e;var c=o?Ou(o,0,f).join(""):n.slice(0,f);if(u===X)return c+e;if(o&&(f+=c.length-f),Ah(u)){if(n.slice(f).search(u)){var a,l=c;for(u.global||(u=sl(u.source,Ec(Nt.exec(u))+"g")),u.lastIndex=0;a=u.exec(l);)var s=a.index;
c=c.slice(0,s===X?f:s)}}else if(n.indexOf(vu(u),f)!=f){var h=c.lastIndexOf(u);h>-1&&(c=c.slice(0,h))}return c+e}function Ia(n){return n=Ec(n),n&&jt.test(n)?n.replace(mt,ye):n}function Ra(n,t,r){return n=Ec(n),t=r?X:t,t===X?$(n)?Q(n):_(n):n.match(t)||[]}function za(t){var r=null==t?0:t.length,e=mi();return t=r?c(t,function(n){if("function"!=typeof n[1])throw new pl(en);return[e(n[0]),n[1]]}):[],uu(function(e){for(var u=-1;++u<r;){var i=t[u];if(n(i[0],this,e))return n(i[1],this,e)}})}function Ea(n){
return Nr(Fr(n,an))}function Sa(n){return function(){return n}}function Wa(n,t){return null==n||n!==n?t:n}function La(n){return n}function Ca(n){return De("function"==typeof n?n:Fr(n,an))}function Ua(n){return qe(Fr(n,an))}function Ba(n,t){return Ze(n,Fr(t,an))}function Ta(n,t,e){var u=Pc(t),i=fe(t,u);null!=e||fc(t)&&(i.length||!u.length)||(e=t,t=n,n=this,i=fe(t,Pc(t)));var o=!(fc(e)&&"chain"in e&&!e.chain),f=uc(n);return r(i,function(r){var e=t[r];n[r]=e,f&&(n.prototype[r]=function(){var t=this.__chain__;
if(o||t){var r=n(this.__wrapped__);return(r.__actions__=Tu(this.__actions__)).push({func:e,args:arguments,thisArg:n}),r.__chain__=t,r}return e.apply(n,a([this.value()],arguments))})}),n}function $a(){return re._===this&&(re._=Al),this}function Da(){}function Ma(n){return n=kc(n),uu(function(t){return Ge(t,n)})}function Fa(n){return Bi(n)?m(no(n)):Qe(n)}function Na(n){return function(t){return null==n?X:_e(n,t)}}function Pa(){return[]}function qa(){return!1}function Za(){return{}}function Ka(){return"";
}function Va(){return!0}function Ga(n,t){if(n=kc(n),n<1||n>Wn)return[];var r=Un,e=Hl(n,Un);t=mi(t),n-=Un;for(var u=O(e,t);++r<n;)t(r);return u}function Ha(n){return bh(n)?c(n,no):bc(n)?[n]:Tu(Cs(Ec(n)))}function Ja(n){var t=++wl;return Ec(n)+t}function Ya(n){return n&&n.length?Yr(n,La,me):X}function Qa(n,t){return n&&n.length?Yr(n,mi(t,2),me):X}function Xa(n){return w(n,La)}function nl(n,t){return w(n,mi(t,2))}function tl(n){return n&&n.length?Yr(n,La,Ne):X}function rl(n,t){return n&&n.length?Yr(n,mi(t,2),Ne):X;
}function el(n){return n&&n.length?k(n,La):0}function ul(n,t){return n&&n.length?k(n,mi(t,2)):0}x=null==x?re:be.defaults(re.Object(),x,be.pick(re,qr));var il=x.Array,ol=x.Date,fl=x.Error,cl=x.Function,al=x.Math,ll=x.Object,sl=x.RegExp,hl=x.String,pl=x.TypeError,_l=il.prototype,vl=cl.prototype,gl=ll.prototype,yl=x["__core-js_shared__"],dl=vl.toString,bl=gl.hasOwnProperty,wl=0,ml=function(){var n=/[^.]+$/.exec(yl&&yl.keys&&yl.keys.IE_PROTO||"");return n?"Symbol(src)_1."+n:""}(),xl=gl.toString,jl=dl.call(ll),Al=re._,kl=sl("^"+dl.call(bl).replace(St,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),Ol=ie?x.Buffer:X,Il=x.Symbol,Rl=x.Uint8Array,zl=Ol?Ol.allocUnsafe:X,El=F(ll.getPrototypeOf,ll),Sl=ll.create,Wl=gl.propertyIsEnumerable,Ll=_l.splice,Cl=Il?Il.isConcatSpreadable:X,Ul=Il?Il.iterator:X,Bl=Il?Il.toStringTag:X,Tl=function(){
try{var n=Ai(ll,"defineProperty");return n({},"",{}),n}catch(n){}}(),$l=x.clearTimeout!==re.clearTimeout&&x.clearTimeout,Dl=ol&&ol.now!==re.Date.now&&ol.now,Ml=x.setTimeout!==re.setTimeout&&x.setTimeout,Fl=al.ceil,Nl=al.floor,Pl=ll.getOwnPropertySymbols,ql=Ol?Ol.isBuffer:X,Zl=x.isFinite,Kl=_l.join,Vl=F(ll.keys,ll),Gl=al.max,Hl=al.min,Jl=ol.now,Yl=x.parseInt,Ql=al.random,Xl=_l.reverse,ns=Ai(x,"DataView"),ts=Ai(x,"Map"),rs=Ai(x,"Promise"),es=Ai(x,"Set"),us=Ai(x,"WeakMap"),is=Ai(ll,"create"),os=us&&new us,fs={},cs=to(ns),as=to(ts),ls=to(rs),ss=to(es),hs=to(us),ps=Il?Il.prototype:X,_s=ps?ps.valueOf:X,vs=ps?ps.toString:X,gs=function(){
function n(){}return function(t){if(!fc(t))return{};if(Sl)return Sl(t);n.prototype=t;var r=new n;return n.prototype=X,r}}();Z.templateSettings={escape:kt,evaluate:Ot,interpolate:It,variable:"",imports:{_:Z}},Z.prototype=J.prototype,Z.prototype.constructor=Z,Y.prototype=gs(J.prototype),Y.prototype.constructor=Y,Ct.prototype=gs(J.prototype),Ct.prototype.constructor=Ct,Xt.prototype.clear=nr,Xt.prototype.delete=tr,Xt.prototype.get=rr,Xt.prototype.has=er,Xt.prototype.set=ur,ir.prototype.clear=or,ir.prototype.delete=fr,
ir.prototype.get=cr,ir.prototype.has=ar,ir.prototype.set=lr,sr.prototype.clear=hr,sr.prototype.delete=pr,sr.prototype.get=_r,sr.prototype.has=vr,sr.prototype.set=gr,yr.prototype.add=yr.prototype.push=dr,yr.prototype.has=br,wr.prototype.clear=mr,wr.prototype.delete=xr,wr.prototype.get=jr,wr.prototype.has=Ar,wr.prototype.set=kr;var ys=Pu(ue),ds=Pu(oe,!0),bs=qu(),ws=qu(!0),ms=os?function(n,t){return os.set(n,t),n}:La,xs=Tl?function(n,t){return Tl(n,"toString",{configurable:!0,enumerable:!1,value:Sa(t),
writable:!0})}:La,js=uu,As=$l||function(n){return re.clearTimeout(n)},ks=es&&1/P(new es([,-0]))[1]==Sn?function(n){return new es(n)}:Da,Os=os?function(n){return os.get(n)}:Da,Is=Pl?function(n){return null==n?[]:(n=ll(n),i(Pl(n),function(t){return Wl.call(n,t)}))}:Pa,Rs=Pl?function(n){for(var t=[];n;)a(t,Is(n)),n=El(n);return t}:Pa,zs=we;(ns&&zs(new ns(new ArrayBuffer(1)))!=ct||ts&&zs(new ts)!=Gn||rs&&zs(rs.resolve())!=Qn||es&&zs(new es)!=tt||us&&zs(new us)!=it)&&(zs=function(n){var t=we(n),r=t==Yn?n.constructor:X,e=r?to(r):"";
if(e)switch(e){case cs:return ct;case as:return Gn;case ls:return Qn;case ss:return tt;case hs:return it}return t});var Es=yl?uc:qa,Ss=Qi(ms),Ws=Ml||function(n,t){return re.setTimeout(n,t)},Ls=Qi(xs),Cs=Pi(function(n){var t=[];return 46===n.charCodeAt(0)&&t.push(""),n.replace(Et,function(n,r,e,u){t.push(e?u.replace(Mt,"$1"):r||n)}),t}),Us=uu(function(n,t){return Jf(n)?Hr(n,ee(t,1,Jf,!0)):[]}),Bs=uu(function(n,t){var r=jo(t);return Jf(r)&&(r=X),Jf(n)?Hr(n,ee(t,1,Jf,!0),mi(r,2)):[]}),Ts=uu(function(n,t){
var r=jo(t);return Jf(r)&&(r=X),Jf(n)?Hr(n,ee(t,1,Jf,!0),X,r):[]}),$s=uu(function(n){var t=c(n,ju);return t.length&&t[0]===n[0]?ke(t):[]}),Ds=uu(function(n){var t=jo(n),r=c(n,ju);return t===jo(r)?t=X:r.pop(),r.length&&r[0]===n[0]?ke(r,mi(t,2)):[]}),Ms=uu(function(n){var t=jo(n),r=c(n,ju);return t="function"==typeof t?t:X,t&&r.pop(),r.length&&r[0]===n[0]?ke(r,X,t):[]}),Fs=uu(Oo),Ns=gi(function(n,t){var r=null==n?0:n.length,e=Tr(n,t);return nu(n,c(t,function(n){return Ci(n,r)?+n:n}).sort(Lu)),e}),Ps=uu(function(n){
return gu(ee(n,1,Jf,!0))}),qs=uu(function(n){var t=jo(n);return Jf(t)&&(t=X),gu(ee(n,1,Jf,!0),mi(t,2))}),Zs=uu(function(n){var t=jo(n);return t="function"==typeof t?t:X,gu(ee(n,1,Jf,!0),X,t)}),Ks=uu(function(n,t){return Jf(n)?Hr(n,t):[]}),Vs=uu(function(n){return mu(i(n,Jf))}),Gs=uu(function(n){var t=jo(n);return Jf(t)&&(t=X),mu(i(n,Jf),mi(t,2))}),Hs=uu(function(n){var t=jo(n);return t="function"==typeof t?t:X,mu(i(n,Jf),X,t)}),Js=uu(Go),Ys=uu(function(n){var t=n.length,r=t>1?n[t-1]:X;return r="function"==typeof r?(n.pop(),
r):X,Ho(n,r)}),Qs=gi(function(n){var t=n.length,r=t?n[0]:0,e=this.__wrapped__,u=function(t){return Tr(t,n)};return!(t>1||this.__actions__.length)&&e instanceof Ct&&Ci(r)?(e=e.slice(r,+r+(t?1:0)),e.__actions__.push({func:nf,args:[u],thisArg:X}),new Y(e,this.__chain__).thru(function(n){return t&&!n.length&&n.push(X),n})):this.thru(u)}),Xs=Fu(function(n,t,r){bl.call(n,r)?++n[r]:Br(n,r,1)}),nh=Ju(ho),th=Ju(po),rh=Fu(function(n,t,r){bl.call(n,r)?n[r].push(t):Br(n,r,[t])}),eh=uu(function(t,r,e){var u=-1,i="function"==typeof r,o=Hf(t)?il(t.length):[];
return ys(t,function(t){o[++u]=i?n(r,t,e):Ie(t,r,e)}),o}),uh=Fu(function(n,t,r){Br(n,r,t)}),ih=Fu(function(n,t,r){n[r?0:1].push(t)},function(){return[[],[]]}),oh=uu(function(n,t){if(null==n)return[];var r=t.length;return r>1&&Ui(n,t[0],t[1])?t=[]:r>2&&Ui(t[0],t[1],t[2])&&(t=[t[0]]),He(n,ee(t,1),[])}),fh=Dl||function(){return re.Date.now()},ch=uu(function(n,t,r){var e=_n;if(r.length){var u=N(r,wi(ch));e|=bn}return ai(n,e,t,r,u)}),ah=uu(function(n,t,r){var e=_n|vn;if(r.length){var u=N(r,wi(ah));e|=bn;
}return ai(t,e,n,r,u)}),lh=uu(function(n,t){return Gr(n,1,t)}),sh=uu(function(n,t,r){return Gr(n,Ic(t)||0,r)});Cf.Cache=sr;var hh=js(function(t,r){r=1==r.length&&bh(r[0])?c(r[0],z(mi())):c(ee(r,1),z(mi()));var e=r.length;return uu(function(u){for(var i=-1,o=Hl(u.length,e);++i<o;)u[i]=r[i].call(this,u[i]);return n(t,this,u)})}),ph=uu(function(n,t){return ai(n,bn,X,t,N(t,wi(ph)))}),_h=uu(function(n,t){return ai(n,wn,X,t,N(t,wi(_h)))}),vh=gi(function(n,t){return ai(n,xn,X,X,X,t)}),gh=ii(me),yh=ii(function(n,t){
return n>=t}),dh=Re(function(){return arguments}())?Re:function(n){return cc(n)&&bl.call(n,"callee")&&!Wl.call(n,"callee")},bh=il.isArray,wh=ce?z(ce):ze,mh=ql||qa,xh=ae?z(ae):Ee,jh=le?z(le):Le,Ah=se?z(se):Be,kh=he?z(he):Te,Oh=pe?z(pe):$e,Ih=ii(Ne),Rh=ii(function(n,t){return n<=t}),zh=Nu(function(n,t){if(Mi(t)||Hf(t))return $u(t,Pc(t),n),X;for(var r in t)bl.call(t,r)&&Sr(n,r,t[r])}),Eh=Nu(function(n,t){$u(t,qc(t),n)}),Sh=Nu(function(n,t,r,e){$u(t,qc(t),n,e)}),Wh=Nu(function(n,t,r,e){$u(t,Pc(t),n,e);
}),Lh=gi(Tr),Ch=uu(function(n,t){n=ll(n);var r=-1,e=t.length,u=e>2?t[2]:X;for(u&&Ui(t[0],t[1],u)&&(e=1);++r<e;)for(var i=t[r],o=qc(i),f=-1,c=o.length;++f<c;){var a=o[f],l=n[a];(l===X||Gf(l,gl[a])&&!bl.call(n,a))&&(n[a]=i[a])}return n}),Uh=uu(function(t){return t.push(X,si),n(Mh,X,t)}),Bh=Xu(function(n,t,r){null!=t&&"function"!=typeof t.toString&&(t=xl.call(t)),n[t]=r},Sa(La)),Th=Xu(function(n,t,r){null!=t&&"function"!=typeof t.toString&&(t=xl.call(t)),bl.call(n,t)?n[t].push(r):n[t]=[r]},mi),$h=uu(Ie),Dh=Nu(function(n,t,r){
Ke(n,t,r)}),Mh=Nu(function(n,t,r,e){Ke(n,t,r,e)}),Fh=gi(function(n,t){var r={};if(null==n)return r;var e=!1;t=c(t,function(t){return t=ku(t,n),e||(e=t.length>1),t}),$u(n,di(n),r),e&&(r=Fr(r,an|ln|sn,hi));for(var u=t.length;u--;)yu(r,t[u]);return r}),Nh=gi(function(n,t){return null==n?{}:Je(n,t)}),Ph=ci(Pc),qh=ci(qc),Zh=Vu(function(n,t,r){return t=t.toLowerCase(),n+(r?fa(t):t)}),Kh=Vu(function(n,t,r){return n+(r?"-":"")+t.toLowerCase()}),Vh=Vu(function(n,t,r){return n+(r?" ":"")+t.toLowerCase()}),Gh=Ku("toLowerCase"),Hh=Vu(function(n,t,r){
return n+(r?"_":"")+t.toLowerCase()}),Jh=Vu(function(n,t,r){return n+(r?" ":"")+Qh(t)}),Yh=Vu(function(n,t,r){return n+(r?" ":"")+t.toUpperCase()}),Qh=Ku("toUpperCase"),Xh=uu(function(t,r){try{return n(t,X,r)}catch(n){return rc(n)?n:new fl(n)}}),np=gi(function(n,t){return r(t,function(t){t=no(t),Br(n,t,ch(n[t],n))}),n}),tp=Yu(),rp=Yu(!0),ep=uu(function(n,t){return function(r){return Ie(r,n,t)}}),up=uu(function(n,t){return function(r){return Ie(n,r,t)}}),ip=ti(c),op=ti(u),fp=ti(h),cp=ui(),ap=ui(!0),lp=ni(function(n,t){
return n+t},0),sp=fi("ceil"),hp=ni(function(n,t){return n/t},1),pp=fi("floor"),_p=ni(function(n,t){return n*t},1),vp=fi("round"),gp=ni(function(n,t){return n-t},0);return Z.after=If,Z.ary=Rf,Z.assign=zh,Z.assignIn=Eh,Z.assignInWith=Sh,Z.assignWith=Wh,Z.at=Lh,Z.before=zf,Z.bind=ch,Z.bindAll=np,Z.bindKey=ah,Z.castArray=Nf,Z.chain=Qo,Z.chunk=uo,Z.compact=io,Z.concat=oo,Z.cond=za,Z.conforms=Ea,Z.constant=Sa,Z.countBy=Xs,Z.create=Sc,Z.curry=Ef,Z.curryRight=Sf,Z.debounce=Wf,Z.defaults=Ch,Z.defaultsDeep=Uh,
Z.defer=lh,Z.delay=sh,Z.difference=Us,Z.differenceBy=Bs,Z.differenceWith=Ts,Z.drop=fo,Z.dropRight=co,Z.dropRightWhile=ao,Z.dropWhile=lo,Z.fill=so,Z.filter=lf,Z.flatMap=sf,Z.flatMapDeep=hf,Z.flatMapDepth=pf,Z.flatten=_o,Z.flattenDeep=vo,Z.flattenDepth=go,Z.flip=Lf,Z.flow=tp,Z.flowRight=rp,Z.fromPairs=yo,Z.functions=$c,Z.functionsIn=Dc,Z.groupBy=rh,Z.initial=mo,Z.intersection=$s,Z.intersectionBy=Ds,Z.intersectionWith=Ms,Z.invert=Bh,Z.invertBy=Th,Z.invokeMap=eh,Z.iteratee=Ca,Z.keyBy=uh,Z.keys=Pc,Z.keysIn=qc,
Z.map=yf,Z.mapKeys=Zc,Z.mapValues=Kc,Z.matches=Ua,Z.matchesProperty=Ba,Z.memoize=Cf,Z.merge=Dh,Z.mergeWith=Mh,Z.method=ep,Z.methodOf=up,Z.mixin=Ta,Z.negate=Uf,Z.nthArg=Ma,Z.omit=Fh,Z.omitBy=Vc,Z.once=Bf,Z.orderBy=df,Z.over=ip,Z.overArgs=hh,Z.overEvery=op,Z.overSome=fp,Z.partial=ph,Z.partialRight=_h,Z.partition=ih,Z.pick=Nh,Z.pickBy=Gc,Z.property=Fa,Z.propertyOf=Na,Z.pull=Fs,Z.pullAll=Oo,Z.pullAllBy=Io,Z.pullAllWith=Ro,Z.pullAt=Ns,Z.range=cp,Z.rangeRight=ap,Z.rearg=vh,Z.reject=mf,Z.remove=zo,Z.rest=Tf,
Z.reverse=Eo,Z.sampleSize=jf,Z.set=Jc,Z.setWith=Yc,Z.shuffle=Af,Z.slice=So,Z.sortBy=oh,Z.sortedUniq=$o,Z.sortedUniqBy=Do,Z.split=da,Z.spread=$f,Z.tail=Mo,Z.take=Fo,Z.takeRight=No,Z.takeRightWhile=Po,Z.takeWhile=qo,Z.tap=Xo,Z.throttle=Df,Z.thru=nf,Z.toArray=jc,Z.toPairs=Ph,Z.toPairsIn=qh,Z.toPath=Ha,Z.toPlainObject=Rc,Z.transform=Qc,Z.unary=Mf,Z.union=Ps,Z.unionBy=qs,Z.unionWith=Zs,Z.uniq=Zo,Z.uniqBy=Ko,Z.uniqWith=Vo,Z.unset=Xc,Z.unzip=Go,Z.unzipWith=Ho,Z.update=na,Z.updateWith=ta,Z.values=ra,Z.valuesIn=ea,
Z.without=Ks,Z.words=Ra,Z.wrap=Ff,Z.xor=Vs,Z.xorBy=Gs,Z.xorWith=Hs,Z.zip=Js,Z.zipObject=Jo,Z.zipObjectDeep=Yo,Z.zipWith=Ys,Z.entries=Ph,Z.entriesIn=qh,Z.extend=Eh,Z.extendWith=Sh,Ta(Z,Z),Z.add=lp,Z.attempt=Xh,Z.camelCase=Zh,Z.capitalize=fa,Z.ceil=sp,Z.clamp=ua,Z.clone=Pf,Z.cloneDeep=Zf,Z.cloneDeepWith=Kf,Z.cloneWith=qf,Z.conformsTo=Vf,Z.deburr=ca,Z.defaultTo=Wa,Z.divide=hp,Z.endsWith=aa,Z.eq=Gf,Z.escape=la,Z.escapeRegExp=sa,Z.every=af,Z.find=nh,Z.findIndex=ho,Z.findKey=Wc,Z.findLast=th,Z.findLastIndex=po,
Z.findLastKey=Lc,Z.floor=pp,Z.forEach=_f,Z.forEachRight=vf,Z.forIn=Cc,Z.forInRight=Uc,Z.forOwn=Bc,Z.forOwnRight=Tc,Z.get=Mc,Z.gt=gh,Z.gte=yh,Z.has=Fc,Z.hasIn=Nc,Z.head=bo,Z.identity=La,Z.includes=gf,Z.indexOf=wo,Z.inRange=ia,Z.invoke=$h,Z.isArguments=dh,Z.isArray=bh,Z.isArrayBuffer=wh,Z.isArrayLike=Hf,Z.isArrayLikeObject=Jf,Z.isBoolean=Yf,Z.isBuffer=mh,Z.isDate=xh,Z.isElement=Qf,Z.isEmpty=Xf,Z.isEqual=nc,Z.isEqualWith=tc,Z.isError=rc,Z.isFinite=ec,Z.isFunction=uc,Z.isInteger=ic,Z.isLength=oc,Z.isMap=jh,
Z.isMatch=ac,Z.isMatchWith=lc,Z.isNaN=sc,Z.isNative=hc,Z.isNil=_c,Z.isNull=pc,Z.isNumber=vc,Z.isObject=fc,Z.isObjectLike=cc,Z.isPlainObject=gc,Z.isRegExp=Ah,Z.isSafeInteger=yc,Z.isSet=kh,Z.isString=dc,Z.isSymbol=bc,Z.isTypedArray=Oh,Z.isUndefined=wc,Z.isWeakMap=mc,Z.isWeakSet=xc,Z.join=xo,Z.kebabCase=Kh,Z.last=jo,Z.lastIndexOf=Ao,Z.lowerCase=Vh,Z.lowerFirst=Gh,Z.lt=Ih,Z.lte=Rh,Z.max=Ya,Z.maxBy=Qa,Z.mean=Xa,Z.meanBy=nl,Z.min=tl,Z.minBy=rl,Z.stubArray=Pa,Z.stubFalse=qa,Z.stubObject=Za,Z.stubString=Ka,
Z.stubTrue=Va,Z.multiply=_p,Z.nth=ko,Z.noConflict=$a,Z.noop=Da,Z.now=fh,Z.pad=ha,Z.padEnd=pa,Z.padStart=_a,Z.parseInt=va,Z.random=oa,Z.reduce=bf,Z.reduceRight=wf,Z.repeat=ga,Z.replace=ya,Z.result=Hc,Z.round=vp,Z.runInContext=p,Z.sample=xf,Z.size=kf,Z.snakeCase=Hh,Z.some=Of,Z.sortedIndex=Wo,Z.sortedIndexBy=Lo,Z.sortedIndexOf=Co,Z.sortedLastIndex=Uo,Z.sortedLastIndexBy=Bo,Z.sortedLastIndexOf=To,Z.startCase=Jh,Z.startsWith=ba,Z.subtract=gp,Z.sum=el,Z.sumBy=ul,Z.template=wa,Z.times=Ga,Z.toFinite=Ac,Z.toInteger=kc,
Z.toLength=Oc,Z.toLower=ma,Z.toNumber=Ic,Z.toSafeInteger=zc,Z.toString=Ec,Z.toUpper=xa,Z.trim=ja,Z.trimEnd=Aa,Z.trimStart=ka,Z.truncate=Oa,Z.unescape=Ia,Z.uniqueId=Ja,Z.upperCase=Yh,Z.upperFirst=Qh,Z.each=_f,Z.eachRight=vf,Z.first=bo,Ta(Z,function(){var n={};return ue(Z,function(t,r){bl.call(Z.prototype,r)||(n[r]=t)}),n}(),{chain:!1}),Z.VERSION=nn,r(["bind","bindKey","curry","curryRight","partial","partialRight"],function(n){Z[n].placeholder=Z}),r(["drop","take"],function(n,t){Ct.prototype[n]=function(r){
r=r===X?1:Gl(kc(r),0);var e=this.__filtered__&&!t?new Ct(this):this.clone();return e.__filtered__?e.__takeCount__=Hl(r,e.__takeCount__):e.__views__.push({size:Hl(r,Un),type:n+(e.__dir__<0?"Right":"")}),e},Ct.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}}),r(["filter","map","takeWhile"],function(n,t){var r=t+1,e=r==Rn||r==En;Ct.prototype[n]=function(n){var t=this.clone();return t.__iteratees__.push({iteratee:mi(n,3),type:r}),t.__filtered__=t.__filtered__||e,t}}),r(["head","last"],function(n,t){
var r="take"+(t?"Right":"");Ct.prototype[n]=function(){return this[r](1).value()[0]}}),r(["initial","tail"],function(n,t){var r="drop"+(t?"":"Right");Ct.prototype[n]=function(){return this.__filtered__?new Ct(this):this[r](1)}}),Ct.prototype.compact=function(){return this.filter(La)},Ct.prototype.find=function(n){return this.filter(n).head()},Ct.prototype.findLast=function(n){return this.reverse().find(n)},Ct.prototype.invokeMap=uu(function(n,t){return"function"==typeof n?new Ct(this):this.map(function(r){
return Ie(r,n,t)})}),Ct.prototype.reject=function(n){return this.filter(Uf(mi(n)))},Ct.prototype.slice=function(n,t){n=kc(n);var r=this;return r.__filtered__&&(n>0||t<0)?new Ct(r):(n<0?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==X&&(t=kc(t),r=t<0?r.dropRight(-t):r.take(t-n)),r)},Ct.prototype.takeRightWhile=function(n){return this.reverse().takeWhile(n).reverse()},Ct.prototype.toArray=function(){return this.take(Un)},ue(Ct.prototype,function(n,t){var r=/^(?:filter|find|map|reject)|While$/.test(t),e=/^(?:head|last)$/.test(t),u=Z[e?"take"+("last"==t?"Right":""):t],i=e||/^find/.test(t);
u&&(Z.prototype[t]=function(){var t=this.__wrapped__,o=e?[1]:arguments,f=t instanceof Ct,c=o[0],l=f||bh(t),s=function(n){var t=u.apply(Z,a([n],o));return e&&h?t[0]:t};l&&r&&"function"==typeof c&&1!=c.length&&(f=l=!1);var h=this.__chain__,p=!!this.__actions__.length,_=i&&!h,v=f&&!p;if(!i&&l){t=v?t:new Ct(this);var g=n.apply(t,o);return g.__actions__.push({func:nf,args:[s],thisArg:X}),new Y(g,h)}return _&&v?n.apply(this,o):(g=this.thru(s),_?e?g.value()[0]:g.value():g)})}),r(["pop","push","shift","sort","splice","unshift"],function(n){
var t=_l[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:pop|shift)$/.test(n);Z.prototype[n]=function(){var n=arguments;if(e&&!this.__chain__){var u=this.value();return t.apply(bh(u)?u:[],n)}return this[r](function(r){return t.apply(bh(r)?r:[],n)})}}),ue(Ct.prototype,function(n,t){var r=Z[t];if(r){var e=r.name+"";bl.call(fs,e)||(fs[e]=[]),fs[e].push({name:t,func:r})}}),fs[Qu(X,vn).name]=[{name:"wrapper",func:X}],Ct.prototype.clone=$t,Ct.prototype.reverse=Yt,Ct.prototype.value=Qt,Z.prototype.at=Qs,
Z.prototype.chain=tf,Z.prototype.commit=rf,Z.prototype.next=ef,Z.prototype.plant=of,Z.prototype.reverse=ff,Z.prototype.toJSON=Z.prototype.valueOf=Z.prototype.value=cf,Z.prototype.first=Z.prototype.head,Ul&&(Z.prototype[Ul]=uf),Z},be=de();"function"==typeof define&&"object"==typeof define.amd&&define.amd?(re._=be,define(function(){return be})):ue?((ue.exports=be)._=be,ee._=be):re._=be}).call(this);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

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