+
+ {(field, index) => (
+
+ )}
+
+
+ );
+}
diff --git a/open-pdf-studio/js/solid/components/SymbolPalette.jsx b/open-pdf-studio/js/solid/components/SymbolPalette.jsx
index 4019ad50..89a4a833 100644
--- a/open-pdf-studio/js/solid/components/SymbolPalette.jsx
+++ b/open-pdf-studio/js/solid/components/SymbolPalette.jsx
@@ -25,6 +25,7 @@ import {
import { registerPaletteDock, unregisterPaletteDock } from '../stores/paletteOrder.js';
import { ifcCategoryForSymbol } from '../data/ifcCategoryMap.js';
import { SYMBOL_STAMP_DEFAULT_SIZE } from '../../annotations/stamp-defaults.js';
+import { setPendingSymbolId } from '../stores/parametricSymbolStore.js';
const DOCK_SNAP = 60;
@@ -89,10 +90,8 @@ function selectSymbol(symbol) {
// click on the page goes through the parametricSymbol tool and the params
// stay editable in the properties panel.
if (symbol.parametricId) {
- import('../stores/parametricSymbolStore.js').then(m => {
- m.setPendingSymbolId(symbol.parametricId);
- setTool('parametricSymbol');
- });
+ setPendingSymbolId(symbol.parametricId);
+ setTool('parametricSymbol');
return;
}
// Generic tool entries: palette items that simply activate a drawing tool
diff --git a/open-pdf-studio/js/solid/components/parametric-label-focus.js b/open-pdf-studio/js/solid/components/parametric-label-focus.js
new file mode 100644
index 00000000..de630279
--- /dev/null
+++ b/open-pdf-studio/js/solid/components/parametric-label-focus.js
@@ -0,0 +1,46 @@
+function makeProgrammaticallyFocusable(target) {
+ if (
+ target
+ && typeof target.hasAttribute === 'function'
+ && typeof target.setAttribute === 'function'
+ && !target.hasAttribute('tabindex')
+ ) {
+ target.setAttribute('tabindex', '-1');
+ }
+ return target;
+}
+
+export function captureParametricLabelReturnFocus(
+ documentRef = document,
+ preferredCanvas = null,
+) {
+ const activeElement = documentRef?.activeElement;
+ if (
+ activeElement
+ && activeElement !== documentRef?.body
+ && activeElement !== documentRef?.documentElement
+ && typeof activeElement.focus === 'function'
+ ) {
+ return activeElement;
+ }
+ const canvas = preferredCanvas?.isConnected === false
+ ? null
+ : preferredCanvas;
+ return makeProgrammaticallyFocusable(
+ canvas || documentRef?.querySelector?.('#annotation-canvas, .annotation-canvas') || null,
+ );
+}
+
+export function restoreParametricLabelFocus(target) {
+ if (!target || target.isConnected === false || typeof target.focus !== 'function') {
+ return false;
+ }
+ try {
+ target.focus({ preventScroll: true });
+ } catch (_) {
+ target.focus();
+ }
+ const documentRef = target.ownerDocument
+ || (typeof document !== 'undefined' ? document : null);
+ return !documentRef?.activeElement || documentRef.activeElement === target;
+}
diff --git a/open-pdf-studio/js/solid/components/parametric-label-outside-events.js b/open-pdf-studio/js/solid/components/parametric-label-outside-events.js
new file mode 100644
index 00000000..4d21c621
--- /dev/null
+++ b/open-pdf-studio/js/solid/components/parametric-label-outside-events.js
@@ -0,0 +1,33 @@
+export function createOutsideCommitController({
+ isActive,
+ commit,
+ isCanvasTarget,
+}) {
+ let pendingOutsideClick = false;
+
+ const isInside = (target, root) =>
+ !!(root && target != null && root.contains(target));
+
+ return {
+ pointerDown(event, root) {
+ pendingOutsideClick = false;
+ if (isInside(event.target, root)) return;
+ if (isCanvasTarget(event.target)) {
+ if (isActive()) commit();
+ return;
+ }
+ pendingOutsideClick = true;
+ },
+
+ click(event, root) {
+ if (!pendingOutsideClick) return;
+ pendingOutsideClick = false;
+ if (isInside(event.target, root) || !isActive()) return;
+ commit();
+ },
+
+ reset() {
+ pendingOutsideClick = false;
+ },
+ };
+}
diff --git a/open-pdf-studio/js/solid/stores/parametricLabelInputStore.js b/open-pdf-studio/js/solid/stores/parametricLabelInputStore.js
new file mode 100644
index 00000000..0565294d
--- /dev/null
+++ b/open-pdf-studio/js/solid/stores/parametricLabelInputStore.js
@@ -0,0 +1,42 @@
+import { createSignal } from 'solid-js';
+
+const [active, setActive] = createSignal(false);
+const [anchor, setAnchor] = createSignal({ left: 0, top: 0 });
+const [fields, setFields] = createSignal([]);
+const [values, setValues] = createSignal({});
+const [onCommit, setOnCommit] = createSignal(null);
+const [onCancel, setOnCancel] = createSignal(null);
+const [locator, setLocator] = createSignal(null);
+const [returnFocusTarget, setReturnFocusTarget] = createSignal(null);
+
+export function showParametricLabelInput(options = {}) {
+ setAnchor(options.anchor || { left: 0, top: 0 });
+ setFields(Array.isArray(options.fields) ? options.fields : []);
+ setValues({ ...(options.values || {}) });
+ setOnCommit(() => (typeof options.commit === 'function' ? options.commit : null));
+ setOnCancel(() => (typeof options.cancel === 'function' ? options.cancel : null));
+ setLocator(() => (typeof options.locate === 'function' ? options.locate : null));
+ setReturnFocusTarget(options.returnFocusTarget || null);
+ setActive(true);
+}
+
+export function hideParametricLabelInput() {
+ setActive(false);
+ setLocator(() => null);
+ setOnCommit(() => null);
+ setOnCancel(() => null);
+ setReturnFocusTarget(null);
+}
+
+export function setFieldValue(key, value) {
+ setValues((current) => ({ ...current, [key]: value }));
+}
+
+export function parametricLabelInputActive() {
+ return active();
+}
+
+export {
+ active, anchor, setAnchor,
+ fields, values, onCommit, onCancel, locator, returnFocusTarget,
+};
diff --git a/open-pdf-studio/js/solid/stores/parametricSymbolStore.js b/open-pdf-studio/js/solid/stores/parametricSymbolStore.js
index 3004db3f..b8e2a650 100644
--- a/open-pdf-studio/js/solid/stores/parametricSymbolStore.js
+++ b/open-pdf-studio/js/solid/stores/parametricSymbolStore.js
@@ -1,17 +1,67 @@
// Tracks the current parametric symbol selection used by the
// parametricSymbol tool when placing a new annotation.
import { createSignal } from 'solid-js';
-import { listTemplates } from '../../symbols/registry.js';
+import { state } from '../../core/state.js';
+import { getTemplate, listTemplates, defaultParams } from '../../symbols/registry.js';
-const [pendingSymbolId, setPendingSymbolId] = createSignal('door');
+const [pendingSymbolId, setPendingSymbolIdSignal] = createSignal('door');
+const [pendingParams, setPendingParamsSignal] = createSignal({});
const [pickerOpen, setPickerOpen] = createSignal(false);
+export function validateSymbolParams(symbolId, values = {}) {
+ const template = getTemplate(symbolId);
+ const defaults = defaultParams(template);
+ if (!template) return {};
+ const result = { ...defaults };
+ for (const def of template.params || []) {
+ const raw = values[def.key];
+ if (raw === undefined) continue;
+ if (def.type === 'number') {
+ const number = Number(raw);
+ if (!Number.isFinite(number)) continue;
+ result[def.key] = Math.min(def.max ?? Infinity, Math.max(def.min ?? -Infinity, number));
+ } else if (def.type === 'boolean') {
+ result[def.key] = raw === true;
+ } else if (def.type === 'enum') {
+ if ((def.options || []).some((option) => option.value === raw)) result[def.key] = raw;
+ } else {
+ result[def.key] = String(raw);
+ }
+ }
+ return result;
+}
+
+export function resolveSymbolParams(symbolId) {
+ return validateSymbolParams(
+ symbolId,
+ state.preferences.parametricSymbolDefaults?.[symbolId] || {},
+ );
+}
+
+export function setPendingSymbolId(symbolId) {
+ setPendingSymbolIdSignal(symbolId);
+ setPendingParamsSignal(resolveSymbolParams(symbolId));
+}
+
+export function setPendingParams(values) {
+ const symbolId = pendingSymbolId();
+ const validated = validateSymbolParams(symbolId, values);
+ setPendingParamsSignal(validated);
+ state.preferences.parametricSymbolDefaults = {
+ ...(state.preferences.parametricSymbolDefaults || {}),
+ [symbolId]: validated,
+ };
+ import('../../core/preferences.js').then((module) => module.savePreferences()).catch(() => {});
+}
+
+setPendingSymbolId('door');
+
function getAvailableTemplates() {
return listTemplates();
}
export {
- pendingSymbolId, setPendingSymbolId,
+ pendingSymbolId, pendingParams,
pickerOpen, setPickerOpen,
getAvailableTemplates,
};
diff --git a/open-pdf-studio/js/solid/stores/propertiesStore.js b/open-pdf-studio/js/solid/stores/propertiesStore.js
index ff752be8..9c3fef50 100644
--- a/open-pdf-studio/js/solid/stores/propertiesStore.js
+++ b/open-pdf-studio/js/solid/stores/propertiesStore.js
@@ -21,6 +21,7 @@ import { syncDocScale } from '../../annotations/scale-bar.js';
import { STAVENREEKS_DEFAULTS } from '../../annotations/stavenreeks.js';
import { recalculateAllMeasurements, calculateArea, calculatePerimeter, calculateDistance, formatMeasurement, formatDimensionText, getMeasureScale } from '../../annotations/measurement.js';
import { applyTemplateRealSize } from '../../symbols/real-size.js';
+import { pendingParams, setPendingParams } from './parametricSymbolStore.js';
// Types whose single 'color' control IS their stroke colour and which render
// via `strokeColor || color`. For these, the 'color' control must mirror onto
@@ -923,6 +924,15 @@ export function updateAnnotProp(key, value) {
if (!currentAnnotation) return;
+ if (currentAnnotation.id === '__tool-defaults__'
+ && currentAnnotation.type === 'parametricSymbol'
+ && key === 'params') {
+ setPendingParams(value);
+ currentAnnotation.params = pendingParams();
+ setAnnotProps('params', currentAnnotation.params);
+ return;
+ }
+
// Tool-defaults mode: user is editing the synthetic annotation that
// showToolDefaults() created. Route writes to state.preferences via
// setAsDefaultStyle so the NEXT annotation drawn picks up the changes,
@@ -1454,6 +1464,12 @@ export function getCurrentAnnotation() {
return currentAnnotation;
}
+export function hideToolDefaults() {
+ if (currentAnnotation?.id !== '__tool-defaults__') return false;
+ storeHideProperties();
+ return true;
+}
+
// Show the properties panel populated with the current style defaults for
// the active drawing tool. Builds a SYNTHETIC annotation tagged with id
// '__tool-defaults__' so the rest of the panel pipeline treats it like a
@@ -1461,7 +1477,7 @@ export function getCurrentAnnotation() {
// Edits made by the user via panel inputs update the synthetic object for
// visual feedback; persistent default changes still flow through the
// Format ribbon's `setAsDefaultStyle` path.
-export async function showToolDefaults(toolName) {
+export async function showToolDefaults(toolName, overrides = {}, shouldShow = null) {
if (!toolName) return;
// Map tool name → annotation type. Most are 1:1; exceptions go here.
const TOOL_TO_TYPE = {
@@ -1514,7 +1530,10 @@ export async function showToolDefaults(toolName) {
// Non-fatal — synthetic will just show the bare defaults above.
}
+ if (typeof shouldShow === 'function' && !shouldShow()) return false;
+ Object.assign(synthetic, overrides);
storeShowProperties(synthetic);
+ return true;
}
export {
diff --git a/open-pdf-studio/js/symbols/editable-labels.js b/open-pdf-studio/js/symbols/editable-labels.js
new file mode 100644
index 00000000..2b196cfd
--- /dev/null
+++ b/open-pdf-studio/js/symbols/editable-labels.js
@@ -0,0 +1,24 @@
+import { getTemplate } from './registry.js';
+
+export function toLocalPoint(annotation, x, y) {
+ const angle = -(Number(annotation.rotation) || 0) * Math.PI / 180;
+ const cx = annotation.x + annotation.width / 2;
+ const cy = annotation.y + annotation.height / 2;
+ const dx = x - cx;
+ const dy = y - cy;
+ return {
+ x: cx + dx * Math.cos(angle) - dy * Math.sin(angle),
+ y: cy + dx * Math.sin(angle) + dy * Math.cos(angle),
+ };
+}
+
+export function findEditableLabel(annotation, x, y) {
+ if (annotation?.type !== 'parametricSymbol') return null;
+ const template = getTemplate(annotation.symbolId);
+ if (typeof template?.editableLabels !== 'function') return null;
+ const point = toLocalPoint(annotation, x, y);
+ const labels = template.editableLabels(annotation.params || {}, annotation);
+ return labels.find(({ rect }) =>
+ point.x >= rect.x && point.x <= rect.x + rect.width
+ && point.y >= rect.y && point.y <= rect.y + rect.height) || null;
+}
diff --git a/open-pdf-studio/js/symbols/templates/wapening-lijn.js b/open-pdf-studio/js/symbols/templates/wapening-lijn.js
index ae1ca319..0df1678f 100644
--- a/open-pdf-studio/js/symbols/templates/wapening-lijn.js
+++ b/open-pdf-studio/js/symbols/templates/wapening-lijn.js
@@ -55,11 +55,10 @@ function lineLayout(params, bbox, centerLine = false) {
};
}
-function rebarLabelCommands(params, layout, isNet, bbox) {
+function rebarLabelLayout(params, layout, isNet, bbox) {
const font = layout.textSize;
const gap = font * 0.20;
const signWidth = font * 0.68;
- const signRadius = font * 0.22;
const left = isNet ? '' : String(integer(params.aantal, 3, 1));
const diameter = compactNumber(positiveNumber(params.diameter, 8, 1));
const lengte = compactNumber(positiveNumber(params.lengte, 1600, 1));
@@ -71,10 +70,23 @@ function rebarLabelCommands(params, layout, isNet, bbox) {
const leftGap = left ? gap : 0;
const totalWidth = leftWidth + leftGap + signWidth + gap + rightWidth;
const desiredX = layout.markerX - totalWidth / 2;
- let cursor = Math.max(
+ const x = Math.max(
bbox.x,
Math.min(desiredX, bbox.x + Math.max(0, bbox.width - totalWidth)),
);
+ return {
+ font, gap, signWidth, signRadius: font * 0.22,
+ left, right, leftWidth, rightWidth, leftGap, totalWidth, x,
+ };
+}
+
+function rebarLabelCommands(params, layout, isNet, bbox) {
+ const metrics = rebarLabelLayout(params, layout, isNet, bbox);
+ const {
+ font, gap, signWidth, signRadius,
+ left, right, leftWidth, rightWidth, leftGap,
+ } = metrics;
+ let cursor = metrics.x;
const commands = [];
if (left) {
@@ -108,6 +120,47 @@ function rebarLabelCommands(params, layout, isNet, bbox) {
return commands;
}
+function editableLineLabels(params, bbox, isNet, centerLine) {
+ const layout = lineLayout(params, bbox, centerLine);
+ const metrics = rebarLabelLayout(params, layout, isNet, bbox);
+ return [{
+ id: 'label',
+ rect: {
+ x: metrics.x,
+ y: layout.textY - metrics.font * 0.6,
+ width: metrics.totalWidth,
+ height: metrics.font * 1.2,
+ },
+ fields: isNet
+ ? ['diameter', 'afstand', 'lengte']
+ : ['aantal', 'diameter', 'lengte'],
+ }];
+}
+
+function markerCommands(params, layout, bbox, enabled) {
+ const count = enabled
+ ? Math.min(4, Math.max(1, Math.round(Number(params.markerAantal) || 1)))
+ : 1;
+ const gap = layout.markerHalfWidth * 0.35;
+ const step = layout.markerHalfWidth * 2 + gap;
+ const groupWidth = step * (count - 1) + layout.markerHalfWidth * 2;
+ const center = Math.max(
+ bbox.x + groupWidth / 2,
+ Math.min(layout.markerX, bbox.x + bbox.width - groupWidth / 2),
+ );
+ return Array.from({ length: count }, (_, index) => {
+ const markerX = center + (index - (count - 1) / 2) * step;
+ return {
+ kind: 'polyline', close: true, fill: true, role: 'marker',
+ points: [
+ { x: markerX - layout.markerHalfWidth, y: layout.markerBaseY },
+ { x: markerX, y: layout.markerTipY },
+ { x: markerX + layout.markerHalfWidth, y: layout.markerBaseY },
+ ],
+ };
+ });
+}
+
function renderLine(params, bbox, isNet, centerLine = false) {
const layout = lineLayout(params, bbox, centerLine);
return [
@@ -118,17 +171,7 @@ function renderLine(params, bbox, isNet, centerLine = false) {
x2: bbox.x + bbox.width,
y2: layout.lineY,
},
- {
- kind: 'polyline',
- close: true,
- fill: true,
- role: 'marker',
- points: [
- { x: layout.markerX - layout.markerHalfWidth, y: layout.markerBaseY },
- { x: layout.markerX, y: layout.markerTipY },
- { x: layout.markerX + layout.markerHalfWidth, y: layout.markerBaseY },
- ],
- },
+ ...markerCommands(params, layout, bbox, !isNet),
...rebarLabelCommands(params, layout, isNet, bbox),
];
}
@@ -169,6 +212,10 @@ export const wapeningsstaafTemplate = {
{ key: 'aantal', label: 'Aantal', labelEn: 'Quantity', type: 'number', default: 3, min: 1, step: 1 },
{ key: 'diameter', label: 'Diameter (mm)', labelEn: 'Diameter (mm)', type: 'number', default: 8, min: 1, step: 1 },
{ key: 'lengte', label: 'Lengte (mm)', labelEn: 'Length (mm)', type: 'number', default: 1600, min: 1, step: 10 },
+ {
+ key: 'markerAantal', label: 'Aantal vlaggen', labelEn: 'Marker count',
+ type: 'number', default: 1, min: 1, max: 4, step: 1,
+ },
...markerParams,
],
layout(params, bbox) {
@@ -177,6 +224,9 @@ export const wapeningsstaafTemplate = {
render(params, bbox) {
return renderLine(params || {}, bbox, false, true);
},
+ editableLabels(params, bbox) {
+ return editableLineLabels(params || {}, bbox, false, true);
+ },
realSizeMm(params) {
return { width: positiveNumber(params?.lengte, 1600, 1), height: 240 };
},
@@ -200,6 +250,9 @@ export const netwapeningTemplate = {
render(params, bbox) {
return renderLine(params || {}, bbox, true);
},
+ editableLabels(params, bbox) {
+ return editableLineLabels(params || {}, bbox, true, false);
+ },
realSizeMm(params) {
return { width: positiveNumber(params?.lengte, 1600, 1), height: 240 };
},
diff --git a/open-pdf-studio/js/symbols/templates/wapeningskorf.js b/open-pdf-studio/js/symbols/templates/wapeningskorf.js
index 2ce3a232..5e3b5f1e 100644
--- a/open-pdf-studio/js/symbols/templates/wapeningskorf.js
+++ b/open-pdf-studio/js/symbols/templates/wapeningskorf.js
@@ -295,6 +295,78 @@ export const wapeningskorfTemplate = {
];
},
+ editableLabels(params, bbox) {
+ const L = layoutMm(params);
+ const W = L.footprint.width;
+ const H = L.footprint.height;
+ const S = Math.min((bbox.width || 1) / W, (bbox.height || 1) / H);
+ const ox = bbox.x + ((bbox.width || 0) - W * S) / 2;
+ const oy = bbox.y + ((bbox.height || 0) - H * S) / 2;
+ const textRect = (x, y, width, font = L.font) => ({
+ x: ox + x * S,
+ y: oy + (y - font * 0.6) * S,
+ width: Math.max(font * 0.5, width) * S,
+ height: font * 1.2 * S,
+ });
+ const labels = [
+ {
+ id: 'boven',
+ fields: ['bovenAantal', 'bovenDiameter'],
+ rect: textRect(
+ L.labels.boven.x, L.labels.boven.y,
+ _barLabelWidth(L.bovenAantal, L.bovenDiameter, L.font),
+ ),
+ },
+ {
+ id: 'zij',
+ fields: ['zijAantal', 'zijDiameter'],
+ rect: textRect(
+ L.labels.zij.x, L.labels.zij.y,
+ _barLabelWidth(L.zijAantal, L.zijDiameter, L.font),
+ ),
+ },
+ {
+ id: 'onder',
+ fields: ['onderAantal', 'onderDiameter'],
+ rect: textRect(
+ L.labels.onder.x, L.labels.onder.y,
+ _barLabelWidth(L.onderAantal, L.onderDiameter, L.font),
+ ),
+ },
+ {
+ id: 'beugel',
+ fields: ['beugelDiameter', 'beugelAfstand'],
+ rect: textRect(
+ L.labels.beugel.x, L.labels.beugel.y,
+ _stirrupLabelWidth(
+ L.prefix, L.beugelDiameter, L.beugelAfstand, L.font,
+ ),
+ ),
+ },
+ {
+ id: 'naam',
+ fields: ['naam'],
+ rect: {
+ ...textRect(
+ L.caption.x, L.caption.y,
+ approxTextWidth(L.naam, L.font * 1.1), L.font * 1.1,
+ ),
+ x: ox + (L.caption.x
+ - approxTextWidth(L.naam, L.font * 1.1) / 2) * S,
+ },
+ },
+ ];
+ const toonLabels = params?.toonLabels !== false;
+ return labels.filter(({ id }) => {
+ if (id === 'naam') return !!L.naam;
+ if (!toonLabels) return false;
+ if (id === 'boven') return L.bovenAantal > 0;
+ if (id === 'zij') return L.zijAantal > 0;
+ if (id === 'onder') return L.onderAantal > 0;
+ return true;
+ });
+ },
+
render(params, bbox) {
const L = layoutMm(params);
const W = L.footprint.width;
diff --git a/open-pdf-studio/js/tools/annotation-creators.js b/open-pdf-studio/js/tools/annotation-creators.js
index c09e86a9..a6e9554b 100644
--- a/open-pdf-studio/js/tools/annotation-creators.js
+++ b/open-pdf-studio/js/tools/annotation-creators.js
@@ -5,10 +5,10 @@ import { snapAngle } from '../utils/helpers.js';
import { calculateDistance, calculateArea, calculatePerimeter, formatMeasurement, snapDistanceTo10 } from '../annotations/measurement.js';
import { getAnnotationType } from '../plugins/annotation-type-registry.js';
import { applyDynamicScaling } from '../annotations/dynamic-scaling.js';
-import { getTemplate, defaultParams } from '../symbols/registry.js';
+import { getTemplate } from '../symbols/registry.js';
import { pxPerMmAt } from '../symbols/real-size.js';
import { syncTwoPointGeometry, syncTwoPointLengthParam } from '../symbols/two-point.js';
-import { pendingSymbolId } from '../solid/stores/parametricSymbolStore.js';
+import { pendingParams, pendingSymbolId } from '../solid/stores/parametricSymbolStore.js';
import { activeCountCategory as _activeCountCategory, nextCountNumber as _nextCountNumber } from '../solid/stores/countStore.js';
import { ifcCategoryForParametric, ifcCategoryForAnnotationType } from '../solid/data/ifcCategoryMap.js';
import { STAVENREEKS_DEFAULTS } from '../annotations/stavenreeks.js';
@@ -348,8 +348,9 @@ export function buildAnnotationProps(tool, startX, startY, endX, endY, e) {
case 'parametricSymbol': {
const symbolId = pendingSymbolId() || 'door';
const template = getTemplate(symbolId);
+ if (!template) return null;
const page = getActiveDocument()?.currentPage || 1;
- const params = template ? defaultParams(template) : {};
+ const params = pendingParams();
if (template?.placement === 'two-point') {
const snappedEnd = snap(startX, startY, endX, endY);
diff --git a/open-pdf-studio/js/tools/manager.js b/open-pdf-studio/js/tools/manager.js
index 8c66c644..06d6bcdd 100644
--- a/open-pdf-studio/js/tools/manager.js
+++ b/open-pdf-studio/js/tools/manager.js
@@ -8,9 +8,11 @@ import { getTool } from './tool-registry.js';
import { buildToolContext, resolvePointerCoords } from './tool-context.js';
import { findAnnotationAt } from '../annotations/geometry.js';
import { findHandleAt } from '../annotations/handles.js';
+import { cancelParametricSymbolInput } from './parametric-symbol-editing.js';
// Tools that are always allowed (view-only, non-modifying)
const READONLY_ALLOWED_TOOLS = new Set(['select', 'hand']);
+let toolDefaultsRequestToken = 0;
// Get cursor for a given tool
export function getCursorForTool(tool = state.currentTool) {
@@ -221,9 +223,11 @@ export function setTool(tool) {
}
}
- // Een openstaande inline stavenreeks-invoer hoort niet te blijven zweven
- // wanneer de gebruiker van gereedschap wisselt.
+ // Openstaande inline invoer hoort niet te blijven zweven wanneer de
+ // gebruiker van gereedschap wisselt. Parametrische invoer moet synchroon
+ // sluiten: de window-click van de editor volgt pas na deze toolhandler.
import('./stavenreeks-editing.js').then(m => m.cancelStavenreeksInput()).catch(() => {});
+ cancelParametricSymbolInput();
// Deactivate PDF text editing when switching away
if (state.currentTool === 'editText' && tool !== 'editText') {
@@ -231,6 +235,9 @@ export function setTool(tool) {
}
state.currentTool = tool;
+ const defaultsRequest = ++toolDefaultsRequestToken;
+ const isCurrentDefaultsRequest = () =>
+ defaultsRequest === toolDefaultsRequestToken && state.currentTool === tool;
// Don't clear toolOverrides when switching TO stamp or wall — the
// SymbolPalette sets them (stamp SVG / wall material+dikte) before setTool.
if (tool !== 'stamp' && tool !== 'wall') {
@@ -290,21 +297,39 @@ export function setTool(tool) {
// (synthetic annotation) so the user can see them BEFORE drawing.
// Otherwise hide the panel (e.g. hand, editText). Select keeps its
// own state (selected annotation or none).
- if (tool !== 'select') {
+ if (tool === 'select') {
+ import('../solid/stores/propertiesStore.js')
+ .then((propStore) => {
+ if (isCurrentDefaultsRequest()) propStore.hideToolDefaults?.();
+ })
+ .catch(() => {});
+ } else {
(async () => {
try {
+ if (tool === 'parametricSymbol') {
+ const symbolStore = await import('../solid/stores/parametricSymbolStore.js');
+ const propStore = await import('../solid/stores/propertiesStore.js');
+ if (!isCurrentDefaultsRequest()) return;
+ await propStore.showToolDefaults(tool, {
+ symbolId: symbolStore.pendingSymbolId(),
+ params: symbolStore.pendingParams(),
+ }, isCurrentDefaultsRequest);
+ return;
+ }
const prefMod = await import('../core/preferences.js');
+ if (!isCurrentDefaultsRequest()) return;
const hasStyle = prefMod && typeof prefMod.getStyleMapping === 'function'
&& prefMod.getStyleMapping(tool) != null;
if (hasStyle) {
const propMod = await import('../solid/stores/propertiesStore.js');
+ if (!isCurrentDefaultsRequest()) return;
if (propMod && typeof propMod.showToolDefaults === 'function') {
- await propMod.showToolDefaults(tool);
+ await propMod.showToolDefaults(tool, {}, isCurrentDefaultsRequest);
return;
}
}
} catch (_) { /* fall through to hide */ }
- hideProperties();
+ if (isCurrentDefaultsRequest()) hideProperties();
})();
}
diff --git a/open-pdf-studio/js/tools/parametric-symbol-editing.js b/open-pdf-studio/js/tools/parametric-symbol-editing.js
new file mode 100644
index 00000000..f381284c
--- /dev/null
+++ b/open-pdf-studio/js/tools/parametric-symbol-editing.js
@@ -0,0 +1,124 @@
+import { getActiveDocument } from '../core/state.js';
+import { annotationCanvas } from '../ui/dom-elements.js';
+import { viewport as viewportState } from '../pdf/pdf-viewport.js';
+import { getTemplate } from '../symbols/registry.js';
+import { findEditableLabel } from '../symbols/editable-labels.js';
+import {
+ showParametricLabelInput, hideParametricLabelInput,
+ parametricLabelInputActive, updateAnnotProp, validateSymbolParams,
+} from '../bridge.js';
+
+let editingAnnotation = null;
+
+function activeCanvas(annotation) {
+ const documentState = getActiveDocument();
+ if (documentState?.viewMode === 'continuous') {
+ return document.querySelector(
+ `.annotation-canvas[data-page="${annotation.page || documentState.currentPage}"]`,
+ );
+ }
+ const annotationPage = annotation.page ?? documentState?.currentPage;
+ if (annotationPage !== documentState?.currentPage) return null;
+ return annotationCanvas || document.getElementById('annotation-canvas');
+}
+
+function rotatedLabelCenter(annotation, label) {
+ const point = {
+ x: label.rect.x + label.rect.width / 2,
+ y: label.rect.y + label.rect.height / 2,
+ };
+ const angle = (Number(annotation.rotation) || 0) * Math.PI / 180;
+ if (!angle) return point;
+ const cx = annotation.x + annotation.width / 2;
+ const cy = annotation.y + annotation.height / 2;
+ const dx = point.x - cx;
+ const dy = point.y - cy;
+ return {
+ x: cx + dx * Math.cos(angle) - dy * Math.sin(angle),
+ y: cy + dx * Math.sin(angle) + dy * Math.cos(angle),
+ };
+}
+
+function labelScreenPosition(annotation, label) {
+ const canvas = activeCanvas(annotation);
+ if (!canvas) return null;
+ const canvasRect = canvas.getBoundingClientRect();
+ const documentState = getActiveDocument();
+ const point = rotatedLabelCenter(annotation, label);
+ const useViewport = documentState?.viewMode !== 'continuous'
+ && viewportState?.active && documentState?.filePath;
+ const scale = useViewport ? viewportState.zoom : (documentState?.scale || 1.5);
+ const offsetX = useViewport ? viewportState.offsetX : 0;
+ const offsetY = useViewport ? viewportState.offsetY : 0;
+ return {
+ left: canvasRect.left + offsetX + point.x * scale,
+ top: canvasRect.top + offsetY + point.y * scale,
+ };
+}
+
+function stillAlive(annotation) {
+ const documentState = getActiveDocument();
+ return Array.isArray(documentState?.annotations)
+ && documentState.annotations.includes(annotation);
+}
+
+export function startParametricSymbolInput(annotation, x, y) {
+ if (!annotation || annotation.type !== 'parametricSymbol' || annotation.locked) return;
+ const label = findEditableLabel(annotation, x, y);
+ if (!label) return;
+ const template = getTemplate(annotation.symbolId);
+ const fieldKeys = new Set(label.fields);
+ const fieldDefinitions = (template?.params || [])
+ .filter((definition) => fieldKeys.has(definition.key));
+ if (!fieldDefinitions.length) return;
+
+ if (editingAnnotation || parametricLabelInputActive()) cancelParametricSymbolInput();
+ const anchor = labelScreenPosition(annotation, label);
+ if (!anchor) return;
+
+ editingAnnotation = annotation;
+ const values = Object.fromEntries(
+ fieldDefinitions.map((definition) => [
+ definition.key,
+ annotation.params?.[definition.key] ?? definition.default ?? '',
+ ]),
+ );
+ const locate = () => {
+ if (!editingAnnotation || !stillAlive(editingAnnotation)) return null;
+ return labelScreenPosition(editingAnnotation, label);
+ };
+ showParametricLabelInput({
+ anchor,
+ fields: fieldDefinitions,
+ values,
+ locate,
+ returnFocusTarget: activeCanvas(annotation),
+ commit: (inputValues) => {
+ const current = editingAnnotation;
+ if (!current || current.locked || locate() === null) {
+ editingAnnotation = null;
+ return;
+ }
+ editingAnnotation = null;
+ const nextParams = {
+ ...(current.params || {}),
+ ...inputValues,
+ };
+ const validated = validateSymbolParams(annotation.symbolId, nextParams);
+ updateAnnotProp('params', validated);
+ },
+ cancel: () => {
+ editingAnnotation = null;
+ },
+ });
+}
+
+export function cancelParametricSymbolInput() {
+ if (!editingAnnotation && !parametricLabelInputActive()) return;
+ editingAnnotation = null;
+ hideParametricLabelInput();
+}
+
+export function isParametricSymbolInputOpen() {
+ return !!editingAnnotation && parametricLabelInputActive();
+}
diff --git a/open-pdf-studio/js/tools/tool-dispatcher.js b/open-pdf-studio/js/tools/tool-dispatcher.js
index 820510a9..28a4c641 100644
--- a/open-pdf-studio/js/tools/tool-dispatcher.js
+++ b/open-pdf-studio/js/tools/tool-dispatcher.js
@@ -337,6 +337,18 @@ export function handleDblClick(e) {
import('./stavenreeks-editing.js')
.then(m => m.startStavenreeksInput(clicked))
.catch(err => console.error('[dispatcher] stavenreeks inline input error', err));
+ } else if (clicked.type === 'parametricSymbol') {
+ state.isDrawing = false;
+ if (dblDoc) {
+ dblDoc.selectedAnnotations = [clicked];
+ dblDoc.selectedAnnotation = clicked;
+ }
+ showProperties(clicked);
+ import('./parametric-symbol-editing.js')
+ .then((module) =>
+ module.startParametricSymbolInput(clicked, coords.x, coords.y))
+ .catch((error) =>
+ console.error('[dispatcher] parametric label input error', error));
} else if (clicked.type === 'comment') {
state.isDrawing = false;
if (dblDoc) { dblDoc.selectedAnnotations = [clicked]; dblDoc.selectedAnnotation = clicked; }
diff --git a/open-pdf-studio/js/types/preferences.ts b/open-pdf-studio/js/types/preferences.ts
index bf2c3732..a560c7f5 100644
--- a/open-pdf-studio/js/types/preferences.ts
+++ b/open-pdf-studio/js/types/preferences.ts
@@ -229,6 +229,7 @@ export interface Preferences {
symbolPaletteFloatY: number;
customSymbolGroups: Array<{ id: string; name: string; symbols: Array<{ id: string; name: string; svg: string }> }>;
disabledSymbolGroups: string[];
+ parametricSymbolDefaults: Record