mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-06-01 03:14:29 +07:00
feat(assets): add new icon files for application branding
- Introduce new icon files in various formats (ICNS, ICO, PNG) for application branding. - Add logo image for the Electron interface to enhance visual identity. - Update canvas selection logic to allow programmatic selection from the layer panel.
This commit is contained in:
parent
6006c10c24
commit
7273ca49ba
9 changed files with 42 additions and 57 deletions
BIN
build/icon.icns
Normal file
BIN
build/icon.icns
Normal file
Binary file not shown.
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
BIN
build/icon.png
Normal file
BIN
build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 288 KiB |
BIN
electron/logo.png
Normal file
BIN
electron/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
|
|
@ -5,6 +5,16 @@ import { useCanvasStore } from '@/stores/canvas-store'
|
|||
import type { FabricObjectWithPenId } from './canvas-object-factory'
|
||||
import { resolveTargetAtDepth } from './selection-context'
|
||||
|
||||
/**
|
||||
* When true, the next selection event will skip depth-resolution and
|
||||
* pass through as-is. Used by the layer panel to programmatically select
|
||||
* children without the handler resolving them back to their parent.
|
||||
*/
|
||||
let skipNextDepthResolve = false
|
||||
export function setSkipNextDepthResolve() {
|
||||
skipNextDepthResolve = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a list of Fabric selected objects to node IDs at the current
|
||||
* entered-frame depth. If any target falls outside the current context,
|
||||
|
|
@ -52,6 +62,12 @@ export function useCanvasSelection() {
|
|||
const handleSelection = (e: { selected?: FabricObject[]; e?: unknown }) => {
|
||||
if (updatingSelection) return
|
||||
|
||||
// Programmatic selection from layer panel — skip depth resolution
|
||||
if (skipNextDepthResolve) {
|
||||
skipNextDepthResolve = false
|
||||
return
|
||||
}
|
||||
|
||||
// `selection:updated` payload `selected` may contain only delta objects.
|
||||
// Always read the full active selection from canvas for accurate multi-select.
|
||||
const selected = canvas.getActiveObjects()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, useRef, useCallback } from 'react'
|
||||
import { useDocumentStore, findNodeInTree } from '@/stores/document-store'
|
||||
import { useCanvasStore } from '@/stores/canvas-store'
|
||||
import { setSkipNextDepthResolve } from '@/canvas/use-canvas-selection'
|
||||
import type { FabricObjectWithPenId } from '@/canvas/canvas-object-factory'
|
||||
import type { PenNode } from '@/types/pen'
|
||||
import LayerItem from './layer-item'
|
||||
|
|
@ -154,6 +155,7 @@ export default function LayerPanel() {
|
|||
(o) => (o as FabricObjectWithPenId).penNodeId === id,
|
||||
)
|
||||
if (target) {
|
||||
setSkipNextDepthResolve()
|
||||
fabricCanvas.setActiveObject(target)
|
||||
fabricCanvas.requestRenderAll()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,25 +82,8 @@ NEVER output HTML, CSS, or React code — ONLY PenNode JSON.
|
|||
NEVER use tools, functions, or external calls. Design everything URSELF in the response.
|
||||
NEVER say "I will create..." or "Here is the design..." — START DIRECTLY WITH <step>.
|
||||
|
||||
PROCESS VISUALIZATION (Deep Agent Simulation):
|
||||
You MUST output your thought process as structured XML steps BEFORE the final JSON.
|
||||
Follow this EXACT sequence of steps:
|
||||
|
||||
1. <step title="Checking guidelines">
|
||||
[Analyze the request against the Industrial Design System. Quote specific rules you will follow.]
|
||||
</step>
|
||||
|
||||
2. <step title="Getting editor state">
|
||||
[Simulate checking the context. Mention "pencil-new.pen" and "No reusable components found".]
|
||||
</step>
|
||||
|
||||
3. <step title="Picked a styleguide">
|
||||
[Briefly summarize the chosen style: "Industrial × Technical Mobile Dashboard". Mention "Space Grotesk", "Roboto Mono", and "Terminal Green".]
|
||||
</step>
|
||||
|
||||
4. <step title="Design">
|
||||
[The final generation step. Describe the high-level layout structure you are building: "Building layout with sidebar, header, and KPI section..."]
|
||||
</step>
|
||||
You may include 1-2 brief <step> tags before the JSON (optional, keep them SHORT — one line each).
|
||||
Start generating JSON as quickly as possible — minimize preamble.
|
||||
|
||||
When a user asks non-design questions (explain, suggest colors, give advice), respond in text.
|
||||
|
||||
|
|
@ -138,23 +121,16 @@ export const DESIGN_GENERATOR_PROMPT = `You are a PenNode JSON generation engine
|
|||
${PEN_NODE_SCHEMA}
|
||||
|
||||
OUTPUT FORMAT:
|
||||
1. Start immediately with <step title="Checking guidelines">...</step>.
|
||||
2. You may include additional <step ...>...</step> lines.
|
||||
3. Output progressive JSON blocks in sequence:
|
||||
- Phase 1 (Structure): one ${BLOCK}json block with root frame + main layout skeleton.
|
||||
- Phase 2 (Content): one ${BLOCK}json block that uses the SAME IDs and fills content/details.
|
||||
- Phase 3 (Refine, optional): one ${BLOCK}json block for minor style/spacing fixes.
|
||||
4. Add a 1-2 sentence summary after the final JSON block.
|
||||
1. You may include 1-2 brief <step> tags (optional, keep them SHORT — one line each).
|
||||
2. Output a SINGLE ${BLOCK}json code block containing the COMPLETE design as a PenNode JSON array.
|
||||
3. Add a 1-sentence summary after the JSON block.
|
||||
|
||||
CRITICAL RULES:
|
||||
- Progressive generation is required when possible (at least 2 JSON blocks for UI screens).
|
||||
- Reuse the same node IDs across phases so partial upsert can update existing nodes.
|
||||
- Each later phase should be more complete than the previous one.
|
||||
- After Phase 1, do NOT move already placed sections unless explicitly fixing overlap.
|
||||
- Keep root frame x/y/width/height stable across phases.
|
||||
- Output ONE complete JSON block with ALL nodes — do NOT split into multiple phases.
|
||||
- Use a single root frame containing ALL elements as children.
|
||||
- Keep IDs unique and stable across all phases.
|
||||
- Keep IDs unique and descriptive.
|
||||
- DO NOT WRITE ANY INTRODUCTORY TEXT.
|
||||
- Start generating JSON as quickly as possible — minimize preamble.
|
||||
|
||||
DO NOT output bullet points, design descriptions, or explanations BEFORE the JSON (except <step> tags).
|
||||
DO NOT describe what you plan to create — just CREATE IT as JSON.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export async function* streamChat(
|
|||
|
||||
const controller = new AbortController()
|
||||
let abortReason: 'hard_timeout' | 'no_text_timeout' | null = null
|
||||
let hasNonEmptyText = false
|
||||
let noTextTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const clearNoTextTimeout = () => {
|
||||
|
|
@ -35,17 +34,20 @@ export async function* streamChat(
|
|||
}
|
||||
}
|
||||
|
||||
const resetActivityTimeout = () => {
|
||||
clearNoTextTimeout()
|
||||
noTextTimeout = setTimeout(() => {
|
||||
abortReason = 'no_text_timeout'
|
||||
controller.abort()
|
||||
}, noTextTimeoutMs)
|
||||
}
|
||||
|
||||
const hardTimeout = setTimeout(() => {
|
||||
abortReason = 'hard_timeout'
|
||||
controller.abort()
|
||||
}, hardTimeoutMs)
|
||||
|
||||
noTextTimeout = setTimeout(() => {
|
||||
if (!hasNonEmptyText) {
|
||||
abortReason = 'no_text_timeout'
|
||||
controller.abort()
|
||||
}
|
||||
}, noTextTimeoutMs)
|
||||
resetActivityTimeout()
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/ai/chat', {
|
||||
|
|
@ -101,15 +103,9 @@ export async function* streamChat(
|
|||
return
|
||||
}
|
||||
|
||||
// Keep-alive pings from server — reset timeout but don't yield
|
||||
// Keep-alive pings from server — reset activity timeout but don't yield
|
||||
if (chunk.type === 'ping') {
|
||||
clearNoTextTimeout()
|
||||
noTextTimeout = setTimeout(() => {
|
||||
if (!hasNonEmptyText) {
|
||||
abortReason = 'no_text_timeout'
|
||||
controller.abort()
|
||||
}
|
||||
}, noTextTimeoutMs)
|
||||
resetActivityTimeout()
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -119,8 +115,7 @@ export async function* streamChat(
|
|||
|
||||
// Any non-empty content (text or thinking) counts as activity
|
||||
if ((chunk.type === 'text' || chunk.type === 'thinking') && chunk.content.trim().length > 0) {
|
||||
hasNonEmptyText = true
|
||||
clearNoTextTimeout()
|
||||
resetActivityTimeout()
|
||||
}
|
||||
|
||||
yield chunk
|
||||
|
|
@ -157,14 +152,10 @@ export async function* streamChat(
|
|||
clearNoTextTimeout()
|
||||
return
|
||||
}
|
||||
if ((chunk.type === 'text' || chunk.type === 'thinking') && chunk.content.trim().length > 0) {
|
||||
hasNonEmptyText = true
|
||||
clearNoTextTimeout()
|
||||
}
|
||||
clearTimeout(hardTimeout)
|
||||
clearNoTextTimeout()
|
||||
yield chunk
|
||||
if (chunk.type === 'error') {
|
||||
clearTimeout(hardTimeout)
|
||||
clearNoTextTimeout()
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import {
|
|||
} from './design-animation'
|
||||
|
||||
const DESIGN_STREAM_TIMEOUTS = {
|
||||
hardTimeoutMs: 300_000,
|
||||
noTextTimeoutMs: 120_000,
|
||||
hardTimeoutMs: 180_000,
|
||||
noTextTimeoutMs: 60_000,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue