feat: enhance designer canvas with multi-view support and model viewer integration
Some checks failed
Deploy Production / deploy (push) Failing after 1m11s

- Added canvasId prop to DesignerCanvas for identifying multiple canvases.
- Implemented active view selection (front, top, left, right) in designer page.
- Updated DesignerCanvas to maintain aspect ratio and dimensions based on view.
- Integrated @google/model-viewer for 3D model rendering on the index page.
- Refactored useSlipmatDesigner to manage multiple canvases and their states.
- Added LAMESA.glb model file for 3D representation.
- Updated package.json and package-lock.json to include @google/model-viewer dependency.
This commit is contained in:
Frank John Begornia
2026-01-12 23:00:32 +08:00
parent 3ba0b250ed
commit 27dabed2d2
7 changed files with 457 additions and 150 deletions

View File

@@ -86,9 +86,11 @@ export const useSlipmatDesigner = () => {
const selectedTemplate = ref<SlipmatTemplate>(FALLBACK_TEMPLATE);
const fabricApi = shallowRef<FabricModule | null>(null);
const canvas = shallowRef<FabricCanvas | null>(null);
const backgroundCircle = shallowRef<FabricCircle | null>(null);
const safeZoneCircle = shallowRef<FabricCircle | null>(null);
const canvases = shallowRef<Record<string, FabricCanvas>>({});
const activeCanvasId = ref<string>('front');
const canvas = computed(() => canvases.value[activeCanvasId.value] || null);
const backgroundCircle = shallowRef<Record<string, FabricCircle | null>>({});
const safeZoneCircle = shallowRef<Record<string, FabricCircle | null>>({});
const previewUrl = ref<string | null>(null);
const previewBlob = shallowRef<Blob | null>(null);
@@ -135,20 +137,32 @@ export const useSlipmatDesigner = () => {
if (!currentCanvas) {
return;
}
if (backgroundCircle.value) {
backgroundCircle.value.set({ radius: currentCanvas.getWidth() / 2 });
const bgCircle = backgroundCircle.value[activeCanvasId.value];
const safeCircle = safeZoneCircle.value[activeCanvasId.value];
if (bgCircle) {
const bgRect = bgCircle as any;
if (bgRect.set) {
bgRect.set({
width: currentCanvas.getWidth(),
height: currentCanvas.getHeight()
});
}
}
if (backgroundCircle.value) {
currentCanvas.sendObjectToBack(backgroundCircle.value);
if (bgCircle) {
currentCanvas.sendObjectToBack(bgCircle);
}
if (safeZoneCircle.value) {
currentCanvas.bringObjectToFront(safeZoneCircle.value);
if (safeCircle) {
currentCanvas.bringObjectToFront(safeCircle);
}
currentCanvas.requestRenderAll();
};
const isStaticObject = (object: FabricObject | null) =>
object === backgroundCircle.value || object === safeZoneCircle.value;
const isStaticObject = (object: FabricObject | null) => {
const bgCircle = backgroundCircle.value[activeCanvasId.value];
const safeCircle = safeZoneCircle.value[activeCanvasId.value];
return object === bgCircle || object === safeCircle;
};
const getSelectedObjects = (): FabricObject[] => {
const currentCanvas = canvas.value;
@@ -222,30 +236,35 @@ export const useSlipmatDesigner = () => {
return;
}
const size = currentCanvas.getWidth();
const width = currentCanvas.getWidth();
const height = currentCanvas.getHeight();
const { backgroundColor, safeZoneInches = 0, diameterInches } =
selectedTemplate.value;
const bgCircle =
backgroundCircle.value ??
new fabric.Circle({
left: size / 2,
top: size / 2,
radius: size / 2,
originX: "center",
originY: "center",
const canvasId = activeCanvasId.value;
const bgRect =
backgroundCircle.value[canvasId] as any ??
new fabric.Rect({
left: 0,
top: 0,
width: width,
height: height,
originX: "left",
originY: "top",
selectable: false,
evented: false,
rx: 8,
ry: 8,
});
bgCircle.set({ fill: backgroundColor });
bgCircle.set({ radius: size / 2 });
bgRect.set({ fill: backgroundColor });
bgRect.set({ width: width, height: height });
if (!backgroundCircle.value) {
backgroundCircle.value = bgCircle;
currentCanvas.add(bgCircle);
if (!backgroundCircle.value[canvasId]) {
backgroundCircle.value[canvasId] = bgRect as any;
currentCanvas.add(bgRect);
}
currentCanvas.sendObjectToBack(bgCircle);
currentCanvas.sendObjectToBack(bgRect);
currentCanvas.requestRenderAll();
const safeRatio = Math.max(
@@ -253,16 +272,17 @@ export const useSlipmatDesigner = () => {
(diameterInches - safeZoneInches * 2) / diameterInches
);
const safeCircleRadius = (size / 2) * safeRatio;
const safeMargin = ((1 - safeRatio) * Math.min(width, height)) / 2;
const safeCircle =
safeZoneCircle.value ??
new fabric.Circle({
left: size / 2,
top: size / 2,
radius: safeCircleRadius,
originX: "center",
originY: "center",
const safeRect =
safeZoneCircle.value[canvasId] as any ??
new fabric.Rect({
left: safeMargin,
top: safeMargin,
width: width - safeMargin * 2,
height: height - safeMargin * 2,
originX: "left",
originY: "top",
fill: "rgba(0,0,0,0)",
stroke: "#4b5563",
strokeDashArray: [8, 8],
@@ -270,36 +290,52 @@ export const useSlipmatDesigner = () => {
selectable: false,
evented: false,
hoverCursor: "default",
rx: 8,
ry: 8,
});
safeCircle.set({ radius: safeCircleRadius });
safeRect.set({
left: safeMargin,
top: safeMargin,
width: width - safeMargin * 2,
height: height - safeMargin * 2
});
if (!safeZoneCircle.value) {
safeZoneCircle.value = safeCircle;
currentCanvas.add(safeCircle);
if (!safeZoneCircle.value[canvasId]) {
safeZoneCircle.value[canvasId] = safeRect as any;
currentCanvas.add(safeRect);
}
maintainStaticLayerOrder();
currentCanvas.requestRenderAll();
currentCanvas.clipPath = new fabric.Circle({
left: size / 2,
top: size / 2,
radius: size / 2,
originX: "center",
originY: "center",
currentCanvas.clipPath = new fabric.Rect({
left: 0,
top: 0,
width: width,
height: height,
originX: "left",
originY: "top",
absolutePositioned: true,
rx: 8,
ry: 8,
});
currentCanvas.renderAll();
schedulePreviewRefresh();
};
const registerCanvas = ({ canvas: instance, fabric }: CanvasReadyPayload) => {
const registerCanvas = ({ canvas: instance, fabric, canvasId = 'front' }: CanvasReadyPayload & { canvasId?: string }) => {
fabricApi.value = fabric;
canvas.value = instance;
canvases.value[canvasId] = instance;
const previousActiveId = activeCanvasId.value;
// Temporarily set this canvas as active to initialize it
activeCanvasId.value = canvasId;
instance.setDimensions({ width: DISPLAY_SIZE, height: DISPLAY_SIZE });
const canvasWidth = DISPLAY_SIZE;
const canvasHeight = Math.round(DISPLAY_SIZE * 0.67); // 3:2 aspect ratio for table
instance.setDimensions({ width: canvasWidth, height: canvasHeight });
instance.preserveObjectStacking = true;
const refreshEvents = [
@@ -352,18 +388,41 @@ export const useSlipmatDesigner = () => {
updateSelectedStyleState();
setZoom(zoomLevel.value);
schedulePreviewRefresh();
// Restore the active canvas to 'front' if this wasn't the front canvas
if (canvasId !== 'front' && previousActiveId) {
activeCanvasId.value = previousActiveId;
} else if (canvasId === 'front') {
// Keep 'front' as active
activeCanvasId.value = 'front';
}
};
const unregisterCanvas = () => {
canvas.value?.dispose();
canvas.value = null;
fabricApi.value = null;
backgroundCircle.value = null;
safeZoneCircle.value = null;
activeFillColor.value = null;
activeStrokeColor.value = null;
hasStyleableSelection.value = false;
zoomLevel.value = 1;
const unregisterCanvas = (canvasId?: string) => {
if (canvasId && canvases.value[canvasId]) {
canvases.value[canvasId]?.dispose();
delete canvases.value[canvasId];
delete backgroundCircle.value[canvasId];
delete safeZoneCircle.value[canvasId];
} else {
// Unregister all
Object.values(canvases.value).forEach(c => c?.dispose());
canvases.value = {};
backgroundCircle.value = {};
safeZoneCircle.value = {};
fabricApi.value = null;
activeFillColor.value = null;
activeStrokeColor.value = null;
hasStyleableSelection.value = false;
zoomLevel.value = 1;
}
};
const setActiveCanvas = (canvasId: string) => {
if (canvases.value[canvasId]) {
activeCanvasId.value = canvasId;
updateSelectedStyleState();
}
};
const centerPoint = () => {
@@ -795,6 +854,7 @@ export const useSlipmatDesigner = () => {
maxZoom: MAX_ZOOM,
registerCanvas,
unregisterCanvas,
setActiveCanvas,
addTextbox,
addShape,
addImageFromFile,