Export IS_BROWSER from core, replace all raw typeof window checks
- Add IS_BROWSER constant to core/constants.ts, rewrite IS_TAURI to use it - Replace 9 raw 'typeof window' checks across canvaskit, fig-export, figma-api, fig-file, fonts, SafariBanner, use-chat - Promote no-typeof-window-check lint rule from warn to error - Extract array mutation helpers (updateArrayItem, removeArrayItem, toggleArrayVisibility) into useMultiProps — DRY up FillSection and StrokeSection (removes 20+ duplicated for-loops)
This commit is contained in:
parent
c06ed32990
commit
921d2d35c5
|
|
@ -223,7 +223,7 @@ const noTypeofWindowCheck = {
|
|||
},
|
||||
create(context) {
|
||||
const file = context.filename ?? context.getFilename?.()
|
||||
if (file?.endsWith('constants.ts') || file?.endsWith('fonts.ts')) return {}
|
||||
if (file?.endsWith('constants.ts')) return {}
|
||||
|
||||
return {
|
||||
BinaryExpression(node) {
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@
|
|||
"open-pencil/no-hand-rolled-color": "error",
|
||||
"open-pencil/no-raw-console-format": "off",
|
||||
"open-pencil/no-silent-catch": "error",
|
||||
"open-pencil/no-typeof-window-check": "warn"
|
||||
"open-pencil/no-typeof-window-check": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import CanvasKitInit, { type CanvasKit } from 'canvaskit-wasm'
|
||||
|
||||
import { IS_BROWSER } from './constants'
|
||||
|
||||
let instance: CanvasKit | null = null
|
||||
|
||||
export type GpuBackend = 'webgl' | 'webgpu'
|
||||
|
|
@ -16,7 +18,7 @@ export interface CanvasKitOptions {
|
|||
}
|
||||
|
||||
function detectBackend(): GpuBackend {
|
||||
if (typeof window === 'undefined') return 'webgl'
|
||||
if (!IS_BROWSER) return 'webgl'
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get('gpu') === 'webgpu' && 'gpu' in navigator) return 'webgpu'
|
||||
return 'webgl'
|
||||
|
|
@ -45,7 +47,7 @@ export async function getCanvasKit(options?: CanvasKitOptions): Promise<CanvasKi
|
|||
activeBackend = backend
|
||||
|
||||
const defaultLocate = (file: string) => {
|
||||
if (typeof window !== 'undefined') return `/${file}`
|
||||
if (IS_BROWSER) return `/${file}`
|
||||
return file
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Color } from './types'
|
||||
|
||||
export const IS_TAURI = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
|
||||
export const IS_BROWSER = typeof window !== 'undefined'
|
||||
export const IS_TAURI = IS_BROWSER && '__TAURI_INTERNALS__' in window
|
||||
|
||||
export const BLACK: Color = { r: 0, g: 0, b: 0, a: 1 }
|
||||
export const TRANSPARENT: Color = { r: 0, g: 0, b: 0, a: 0 }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { zipSync, deflateSync, type Zippable } from 'fflate'
|
||||
|
||||
import { CANVAS_BG_COLOR, IS_TAURI } from './constants'
|
||||
import { CANVAS_BG_COLOR, IS_BROWSER, IS_TAURI } from './constants'
|
||||
import { sceneNodeToKiwi, fractionalPosition, buildFigKiwi, buildFontDigestMap } from './kiwi-serialize'
|
||||
import { initCodec, getCompiledSchema, getSchemaBytes } from './kiwi/codec'
|
||||
import { stringToGuid } from './kiwi/kiwi-convert'
|
||||
|
|
@ -271,7 +271,7 @@ export function compressFigDataSync(
|
|||
}
|
||||
|
||||
function canUseWorker(): boolean {
|
||||
return typeof Worker !== 'undefined' && typeof window !== 'undefined'
|
||||
return typeof Worker !== 'undefined' && IS_BROWSER
|
||||
}
|
||||
|
||||
function compressViaWorker(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
VariableValue
|
||||
} from './scene-graph'
|
||||
import type { Rect, Vector } from './types'
|
||||
import { IS_BROWSER } from './constants'
|
||||
import { copyFills, copyStrokes, copyEffects } from './copy'
|
||||
|
||||
import {
|
||||
|
|
@ -337,8 +338,8 @@ export class FigmaAPI implements NodeProxyHost {
|
|||
const padding = 80
|
||||
const contentW = maxX - minX + padding * 2
|
||||
const contentH = maxY - minY + padding * 2
|
||||
const viewW = typeof window !== 'undefined' ? window.innerWidth : 1280
|
||||
const viewH = typeof window !== 'undefined' ? window.innerHeight : 720
|
||||
const viewW = IS_BROWSER ? window.innerWidth : 1280
|
||||
const viewH = IS_BROWSER ? window.innerHeight : 720
|
||||
const zoom = Math.min(viewW / contentW, viewH / contentH, 1)
|
||||
this._viewport = { x: (minX + maxX) / 2, y: (minY + maxY) / 2, zoom }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { CanvasKit, TypefaceFontProvider } from 'canvaskit-wasm'
|
|||
|
||||
import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
IS_BROWSER,
|
||||
CJK_FALLBACK_FAMILIES_MACOS,
|
||||
CJK_FALLBACK_FAMILIES_WINDOWS,
|
||||
CJK_FALLBACK_FAMILIES_LINUX,
|
||||
|
|
@ -17,8 +18,6 @@ export interface FontInfo {
|
|||
postscriptName: string
|
||||
}
|
||||
|
||||
const IS_BROWSER = typeof window !== 'undefined'
|
||||
|
||||
const loadedFamilies = new Map<string, ArrayBuffer>()
|
||||
let fontProvider: TypefaceFontProvider | null = null
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { unzipSync, inflateSync } from 'fflate'
|
||||
import { decompress as zstdDecompress } from 'fzstd'
|
||||
|
||||
import { IS_BROWSER } from '../constants'
|
||||
import { importNodeChanges } from './fig-import'
|
||||
import { decodeBinarySchema, compileSchema, ByteBuffer } from './kiwi-schema'
|
||||
import { isZstdCompressed } from './protocol'
|
||||
|
|
@ -129,7 +130,7 @@ function parseViaWorker(buffer: ArrayBuffer): Promise<SceneGraph> {
|
|||
}
|
||||
|
||||
export async function parseFigFile(buffer: ArrayBuffer): Promise<SceneGraph> {
|
||||
if (typeof Worker !== 'undefined' && typeof window !== 'undefined') {
|
||||
if (typeof Worker !== 'undefined' && IS_BROWSER) {
|
||||
const copy = buffer.slice(0)
|
||||
try {
|
||||
return await parseViaWorker(buffer)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
import { IS_TAURI } from '@/constants'
|
||||
import { IS_BROWSER, IS_TAURI } from '@/constants'
|
||||
|
||||
const dismissed = useLocalStorage('safari-banner-dismissed', false)
|
||||
const show = !IS_TAURI && typeof window !== 'undefined' && !window.showSaveFilePicker
|
||||
const show = !IS_TAURI && IS_BROWSER && !window.showSaveFilePicker
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { colorToCSS, colorToHexRaw } from '@open-pencil/core'
|
|||
import type { Fill, Variable, Color } from '@open-pencil/core'
|
||||
|
||||
const { store } = useNodeProps()
|
||||
const { nodes, isMulti, active, activeNode, isArrayMixed } = useMultiProps()
|
||||
const { nodes, isMulti, active, activeNode, targetNodes, isArrayMixed, updateArrayItem, removeArrayItem, toggleArrayVisibility } = useMultiProps()
|
||||
|
||||
const fillsAreMixed = computed(() => isArrayMixed('fills'))
|
||||
|
||||
|
|
@ -56,50 +56,26 @@ function resolvedSwatchStyle(variable: Variable): string {
|
|||
}
|
||||
|
||||
function updateFill(index: number, fill: Fill) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
const fills = [...n.fills]
|
||||
fills[index] = fill
|
||||
store.updateNodeWithUndo(n.id, { fills }, 'Change fill')
|
||||
}
|
||||
updateArrayItem('fills', index, fill, 'Change fill')
|
||||
}
|
||||
|
||||
function updateOpacity(index: number, opacity: number) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
const fills = [...n.fills]
|
||||
fills[index] = { ...fills[index], opacity: Math.max(0, Math.min(1, opacity / 100)) }
|
||||
store.updateNodeWithUndo(n.id, { fills }, 'Change fill')
|
||||
}
|
||||
updateArrayItem('fills', index, { opacity: Math.max(0, Math.min(1, opacity / 100)) }, 'Change fill')
|
||||
}
|
||||
|
||||
function toggleVisibility(index: number) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
const fills = [...n.fills]
|
||||
fills[index] = { ...fills[index], visible: !fills[index].visible }
|
||||
store.updateNodeWithUndo(n.id, { fills }, 'Change fill')
|
||||
}
|
||||
toggleArrayVisibility('fills', index)
|
||||
}
|
||||
|
||||
function add() {
|
||||
if (isMulti.value) {
|
||||
for (const n of nodes.value) {
|
||||
store.updateNodeWithUndo(n.id, { fills: [{ ...DEFAULT_SHAPE_FILL }] }, 'Set fill')
|
||||
}
|
||||
store.requestRender()
|
||||
} else {
|
||||
const n = activeNode.value
|
||||
if (!n) return
|
||||
store.updateNodeWithUndo(n.id, { fills: [...n.fills, { ...DEFAULT_SHAPE_FILL }] }, 'Add fill')
|
||||
for (const n of targetNodes()) {
|
||||
const fills = isMulti.value ? [{ ...DEFAULT_SHAPE_FILL }] : [...n.fills, { ...DEFAULT_SHAPE_FILL }]
|
||||
store.updateNodeWithUndo(n.id, { fills }, isMulti.value ? 'Set fill' : 'Add fill')
|
||||
}
|
||||
}
|
||||
|
||||
function remove(index: number) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
store.updateNodeWithUndo(n.id, { fills: n.fills.filter((_, i) => i !== index) }, 'Remove fill')
|
||||
}
|
||||
removeArrayItem('fills', index, 'Remove fill')
|
||||
}
|
||||
|
||||
const searchTerm = ref('')
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import type { Color, SceneNode, Stroke } from '@open-pencil/core'
|
|||
type StrokeSides = 'ALL' | 'TOP' | 'BOTTOM' | 'LEFT' | 'RIGHT' | 'CUSTOM'
|
||||
|
||||
const { store } = useNodeProps()
|
||||
const { nodes, isMulti, active, activeNode, isArrayMixed } = useMultiProps()
|
||||
const { nodes, isMulti, active, activeNode, targetNodes, isArrayMixed, updateArrayItem, removeArrayItem, toggleArrayVisibility } = useMultiProps()
|
||||
|
||||
const strokesAreMixed = computed(() => isArrayMixed('strokes'))
|
||||
|
||||
|
|
@ -59,47 +59,26 @@ const hasStrokes = computed(
|
|||
const sideMenuOpen = ref(false)
|
||||
|
||||
function updateColor(index: number, color: Color) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
const strokes = [...n.strokes]
|
||||
strokes[index] = { ...strokes[index], color }
|
||||
store.updateNodeWithUndo(n.id, { strokes }, 'Change stroke')
|
||||
}
|
||||
updateArrayItem('strokes', index, { color }, 'Change stroke')
|
||||
}
|
||||
|
||||
function updateWeight(index: number, weight: number) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
const strokes = [...n.strokes]
|
||||
strokes[index] = { ...strokes[index], weight }
|
||||
store.updateNodeWithUndo(n.id, { strokes }, 'Change stroke')
|
||||
}
|
||||
updateArrayItem('strokes', index, { weight }, 'Change stroke')
|
||||
}
|
||||
|
||||
function updateOpacity(index: number, opacity: number) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
const strokes = [...n.strokes]
|
||||
strokes[index] = { ...strokes[index], opacity: Math.max(0, Math.min(1, opacity / 100)) }
|
||||
store.updateNodeWithUndo(n.id, { strokes }, 'Change stroke')
|
||||
}
|
||||
updateArrayItem('strokes', index, { opacity: Math.max(0, Math.min(1, opacity / 100)) }, 'Change stroke')
|
||||
}
|
||||
|
||||
function updateAlign(align: Stroke['align']) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
for (const n of targetNodes()) {
|
||||
const strokes = n.strokes.map((s) => ({ ...s, align }))
|
||||
store.updateNodeWithUndo(n.id, { strokes }, 'Change stroke align')
|
||||
}
|
||||
}
|
||||
|
||||
function toggleVisibility(index: number) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
const strokes = [...n.strokes]
|
||||
strokes[index] = { ...strokes[index], visible: !strokes[index].visible }
|
||||
store.updateNodeWithUndo(n.id, { strokes }, 'Change stroke')
|
||||
}
|
||||
toggleArrayVisibility('strokes', index)
|
||||
}
|
||||
|
||||
function add() {
|
||||
|
|
@ -110,32 +89,18 @@ function add() {
|
|||
visible: true,
|
||||
align: 'CENTER'
|
||||
}
|
||||
if (isMulti.value) {
|
||||
for (const n of nodes.value) {
|
||||
store.updateNodeWithUndo(n.id, { strokes: [stroke] }, 'Set stroke')
|
||||
}
|
||||
store.requestRender()
|
||||
} else {
|
||||
const n = activeNode.value
|
||||
if (!n) return
|
||||
store.updateNodeWithUndo(n.id, { strokes: [...n.strokes, stroke] }, 'Add stroke')
|
||||
for (const n of targetNodes()) {
|
||||
const strokes = isMulti.value ? [stroke] : [...n.strokes, stroke]
|
||||
store.updateNodeWithUndo(n.id, { strokes }, isMulti.value ? 'Set stroke' : 'Add stroke')
|
||||
}
|
||||
}
|
||||
|
||||
function remove(index: number) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
store.updateNodeWithUndo(
|
||||
n.id,
|
||||
{ strokes: n.strokes.filter((_, i) => i !== index) },
|
||||
'Remove stroke'
|
||||
)
|
||||
}
|
||||
removeArrayItem('strokes', index, 'Remove stroke')
|
||||
}
|
||||
|
||||
function selectSide(side: StrokeSides) {
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
for (const n of targetNodes()) {
|
||||
const weight = n.strokes.length > 0 ? n.strokes[0].weight : 1
|
||||
if (side === 'ALL') {
|
||||
store.updateNodeWithUndo(
|
||||
|
|
@ -193,8 +158,7 @@ function updateBorderWeight(side: 'top' | 'right' | 'bottom' | 'left', value: nu
|
|||
bottom: 'borderBottomWeight',
|
||||
left: 'borderLeftWeight'
|
||||
} as const
|
||||
for (const n of isMulti.value ? nodes.value : [activeNode.value]) {
|
||||
if (!n) continue
|
||||
for (const n of targetNodes()) {
|
||||
store.updateNodeWithUndo(
|
||||
n.id,
|
||||
{ [fieldMap[side]]: value } as Partial<SceneNode>,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
AI_PROVIDERS,
|
||||
DEFAULT_AI_MODEL,
|
||||
DEFAULT_AI_PROVIDER,
|
||||
IS_BROWSER,
|
||||
IS_TAURI,
|
||||
setPexelsApiKey,
|
||||
setUnsplashAccessKey
|
||||
|
|
@ -41,7 +42,7 @@ function migrateLegacyStorage() {
|
|||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') migrateLegacyStorage()
|
||||
if (IS_BROWSER) migrateLegacyStorage()
|
||||
|
||||
const providerID = useLocalStorage<AIProviderID>(
|
||||
`${STORAGE_PREFIX}ai-provider`,
|
||||
|
|
@ -270,7 +271,7 @@ function resetChat() {
|
|||
transportDirty = false
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
if (IS_BROWSER) {
|
||||
window.__OPEN_PENCIL_SET_TRANSPORT__ = (factory) => {
|
||||
overrideTransport = factory
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,46 @@ export function useMultiProps() {
|
|||
return all.some((n) => JSON.stringify(n[key]) !== first)
|
||||
}
|
||||
|
||||
type ArrayPropKey = 'fills' | 'strokes' | 'effects'
|
||||
|
||||
function targetNodes(): SceneNode[] {
|
||||
if (isMulti.value) return nodes.value
|
||||
return activeNode.value ? [activeNode.value] : []
|
||||
}
|
||||
|
||||
function updateArrayItem(
|
||||
key: ArrayPropKey,
|
||||
index: number,
|
||||
patch: Record<string, unknown>,
|
||||
label: string
|
||||
) {
|
||||
for (const n of targetNodes()) {
|
||||
const arr = [...n[key]]
|
||||
arr[index] = { ...arr[index], ...patch } as (typeof arr)[number]
|
||||
store.updateNodeWithUndo(n.id, { [key]: arr } as Partial<SceneNode>, label)
|
||||
}
|
||||
}
|
||||
|
||||
function removeArrayItem(key: ArrayPropKey, index: number, label: string) {
|
||||
for (const n of targetNodes()) {
|
||||
store.updateNodeWithUndo(
|
||||
n.id,
|
||||
{ [key]: (n[key] as unknown[]).filter((_, i) => i !== index) } as Partial<SceneNode>,
|
||||
label
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleArrayVisibility(key: ArrayPropKey, index: number) {
|
||||
for (const n of targetNodes()) {
|
||||
const items = n[key] as Array<{ visible: boolean }>
|
||||
if (!items[index]) continue
|
||||
const arr = [...n[key]]
|
||||
arr[index] = { ...arr[index], visible: !items[index].visible }
|
||||
store.updateNodeWithUndo(n.id, { [key]: arr } as Partial<SceneNode>, `Toggle ${key} visibility`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
store,
|
||||
node,
|
||||
|
|
@ -50,9 +90,13 @@ export function useMultiProps() {
|
|||
isMulti,
|
||||
active,
|
||||
activeNode,
|
||||
targetNodes,
|
||||
prop,
|
||||
merged,
|
||||
updateAllWithUndo,
|
||||
updateArrayItem,
|
||||
removeArrayItem,
|
||||
toggleArrayVisibility,
|
||||
isArrayMixed
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export {
|
||||
IS_BROWSER,
|
||||
IS_TAURI,
|
||||
SELECTION_COLOR,
|
||||
COMPONENT_COLOR,
|
||||
|
|
|
|||
Loading…
Reference in a new issue