From 638db3858926e8fb2a34847a5e6964e496d19c5d Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Tue, 17 Mar 2026 09:43:26 +0300 Subject: [PATCH] =?UTF-8?q?Phase=203:=20Property=20primitives=20=E2=80=94?= =?UTF-8?q?=20PropertyList,=20PositionControls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New SDK components: - PropertyListRoot / PropertyListItem — headless array property editor for fills, strokes, effects. Provides add/remove/update/patch/ toggleVisibility via slots. Handles multi-selection, mixed state. - PositionControlsRoot — headless position/alignment/flip/rotate. Provides x/y/w/h/rotation + updateProp/commitProp/align/flip/rotate. App FillSection.vue rewritten to compose from PropertyListRoot — no more useNodeProps() destructuring, CRUD logic in SDK. SkiaRenderer.renderFromEditorState() — single-call render that takes EditorState directly (was 30 lines of manual property sync). --- .../PositionControls/PositionControlsRoot.vue | 64 ++++ packages/vue/src/PositionControls/index.ts | 1 + .../vue/src/PropertyList/PropertyListItem.vue | 18 ++ .../vue/src/PropertyList/PropertyListRoot.vue | 111 +++++++ packages/vue/src/PropertyList/context.ts | 32 ++ packages/vue/src/PropertyList/index.ts | 4 + packages/vue/src/index.ts | 5 + src/components/properties/FillSection.vue | 293 ++++++++---------- 8 files changed, 366 insertions(+), 162 deletions(-) create mode 100644 packages/vue/src/PositionControls/PositionControlsRoot.vue create mode 100644 packages/vue/src/PositionControls/index.ts create mode 100644 packages/vue/src/PropertyList/PropertyListItem.vue create mode 100644 packages/vue/src/PropertyList/PropertyListRoot.vue create mode 100644 packages/vue/src/PropertyList/context.ts create mode 100644 packages/vue/src/PropertyList/index.ts diff --git a/packages/vue/src/PositionControls/PositionControlsRoot.vue b/packages/vue/src/PositionControls/PositionControlsRoot.vue new file mode 100644 index 000000000..ac79b996e --- /dev/null +++ b/packages/vue/src/PositionControls/PositionControlsRoot.vue @@ -0,0 +1,64 @@ + + + diff --git a/packages/vue/src/PositionControls/index.ts b/packages/vue/src/PositionControls/index.ts new file mode 100644 index 000000000..9fb0f748a --- /dev/null +++ b/packages/vue/src/PositionControls/index.ts @@ -0,0 +1 @@ +export { default as PositionControlsRoot } from './PositionControlsRoot.vue' diff --git a/packages/vue/src/PropertyList/PropertyListItem.vue b/packages/vue/src/PropertyList/PropertyListItem.vue new file mode 100644 index 000000000..40229a3be --- /dev/null +++ b/packages/vue/src/PropertyList/PropertyListItem.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/vue/src/PropertyList/PropertyListRoot.vue b/packages/vue/src/PropertyList/PropertyListRoot.vue new file mode 100644 index 000000000..c94850ae7 --- /dev/null +++ b/packages/vue/src/PropertyList/PropertyListRoot.vue @@ -0,0 +1,111 @@ + + + diff --git a/packages/vue/src/PropertyList/context.ts b/packages/vue/src/PropertyList/context.ts new file mode 100644 index 000000000..cacbd06d8 --- /dev/null +++ b/packages/vue/src/PropertyList/context.ts @@ -0,0 +1,32 @@ +import { type InjectionKey, inject, provide } from 'vue' + +import type { Editor } from '@open-pencil/core/editor' +import type { SceneNode } from '@open-pencil/core' +import type { ComputedRef } from 'vue' + +type ArrayPropKey = 'fills' | 'strokes' | 'effects' + +export interface PropertyListContext { + editor: Editor + propKey: ArrayPropKey + items: ComputedRef + isMixed: ComputedRef + activeNode: ComputedRef + isMulti: ComputedRef + add: (defaults: unknown) => void + remove: (index: number) => void + update: (index: number, item: unknown) => void + toggleVisibility: (index: number) => void +} + +export const PROPERTY_LIST_KEY: InjectionKey = Symbol('property-list') + +export function providePropertyList(ctx: PropertyListContext) { + provide(PROPERTY_LIST_KEY, ctx) +} + +export function usePropertyList(): PropertyListContext { + const ctx = inject(PROPERTY_LIST_KEY) + if (!ctx) throw new Error('[open-pencil] usePropertyList() called outside ') + return ctx +} diff --git a/packages/vue/src/PropertyList/index.ts b/packages/vue/src/PropertyList/index.ts new file mode 100644 index 000000000..f089cbd75 --- /dev/null +++ b/packages/vue/src/PropertyList/index.ts @@ -0,0 +1,4 @@ +export { default as PropertyListRoot } from './PropertyListRoot.vue' +export { default as PropertyListItem } from './PropertyListItem.vue' +export { usePropertyList } from './context' +export type { PropertyListContext } from './context' diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 5f6ab8141..5eb4d84d5 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -27,6 +27,11 @@ export { export type { ScrubInputContext } from './ScrubInput' export { CanvasRoot, CanvasSurface, useCanvasContext } from './Canvas' + +export { PropertyListRoot, PropertyListItem, usePropertyList } from './PropertyList' +export type { PropertyListContext } from './PropertyList' + +export { PositionControlsRoot } from './PositionControls' export type { CanvasContext } from './Canvas' export { ToolbarRoot, ToolbarItem, useToolbar } from './Toolbar' diff --git a/src/components/properties/FillSection.vue b/src/components/properties/FillSection.vue index be22e1b74..85bc1a69c 100644 --- a/src/components/properties/FillSection.vue +++ b/src/components/properties/FillSection.vue @@ -15,45 +15,31 @@ import { import FillPicker from '@/components/FillPicker.vue' import ScrubInput from '@/components/ScrubInput.vue' -import { useNodeProps } from '@open-pencil/vue' +import { PropertyListRoot } from '@open-pencil/vue' +import { useEditorStore } from '@/stores/editor' import { DEFAULT_SHAPE_FILL } from '@/constants' import { colorToCSS, colorToHexRaw } from '@open-pencil/core' + import type { Fill, Variable } from '@open-pencil/core' -const { - store, - isMulti, - active, - activeNode, - targetNodes, - isArrayMixed, - updateArrayItem, - removeArrayItem, - toggleArrayVisibility -} = useNodeProps() - -const fillsAreMixed = computed(() => isArrayMixed('fills')) +const store = useEditorStore() const colorVariables = computed(() => store.graph.getVariablesByType('COLOR')) -function getBoundVariable(index: number): Variable | undefined { - const n = activeNode.value +function getBoundVariable(nodeId: string, index: number): Variable | undefined { + const n = store.graph.getNode(nodeId) if (!n) return undefined const varId = n.boundVariables[`fills/${index}/color`] return varId ? store.graph.variables.get(varId) : undefined } -function bindVariable(index: number, variableId: string) { - const n = activeNode.value - if (!n) return - store.graph.bindVariable(n.id, `fills/${index}/color`, variableId) +function bindVariable(nodeId: string, index: number, variableId: string) { + store.graph.bindVariable(nodeId, `fills/${index}/color`, variableId) store.requestRender() } -function unbindVariable(index: number) { - const n = activeNode.value - if (!n) return - store.graph.unbindVariable(n.id, `fills/${index}/color`) +function unbindVariable(nodeId: string, index: number) { + store.graph.unbindVariable(nodeId, `fills/${index}/color`) store.requestRender() } @@ -63,36 +49,6 @@ function resolvedSwatchStyle(variable: Variable): string { return `background: ${colorToCSS(color)}` } -function updateFill(index: number, fill: Fill) { - updateArrayItem('fills', index, fill, 'Change fill') -} - -function updateOpacity(index: number, opacity: number) { - updateArrayItem( - 'fills', - index, - { opacity: Math.max(0, Math.min(1, opacity / 100)) }, - 'Change fill' - ) -} - -function toggleVisibility(index: number) { - toggleArrayVisibility('fills', index) -} - -function add() { - 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) { - removeArrayItem('fills', index, 'Remove fill') -} - const searchTerm = ref('') const { contains } = useFilter({ sensitivity: 'base' }) const filteredVariables = computed(() => { @@ -102,119 +58,132 @@ const filteredVariables = computed(() => {